mirror of
https://github.com/azaion/annotations.git
synced 2026-04-23 04:16:30 +00:00
Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e090f2d093 | |||
| 51248edbd3 | |||
| 5e226d422d | |||
| 99b9058187 | |||
| 9b80eaf435 | |||
| 429b39ee32 | |||
| 3a6ed60ea0 | |||
| 5500bda6ce | |||
| 9e25110016 | |||
| 7311f08884 | |||
| 2ee85d2e64 | |||
| e501279b91 | |||
| 0549c2de7e | |||
| 1d32c224ba | |||
| 2dc60a7ef4 | |||
| fde9a9f418 | |||
| b0e4b467c1 | |||
| 053719c4a8 | |||
| 91ffa694f9 | |||
| 2ba2b0aa8d | |||
| 9a16099194 | |||
| 7d68f7faee | |||
| be77a81875 | |||
| b3665630ed | |||
| a7a99c49c0 | |||
| 7bbdb61253 | |||
| 9b93e39d37 | |||
| b2b2efe120 | |||
| d396677451 | |||
| 5ef81fab22 | |||
| b82de5f573 | |||
| b6b6751c37 | |||
| fa3f31b147 | |||
| c061260620 | |||
| 067f02cc63 | |||
| d1ce9d9365 | |||
| eb9e2a6f47 | |||
| 55d8a5cb85 | |||
| 61c93e9c88 | |||
| 4780e8c61c | |||
| 16e5853d67 | |||
| 9e4dc5404c | |||
| ad782bcbaa | |||
| fc6e5db795 | |||
| fefd054ea0 | |||
| 938fd36aec | |||
| 6229ca8a03 | |||
| 75d3a2412f | |||
| f6f0b0b266 | |||
| 291398a318 | |||
| 074c098d67 | |||
| a68156a4d5 | |||
| ea7c487493 | |||
| eac6460645 | |||
| bb6413c4f0 | |||
| 6eeb6d20bf | |||
| c07cac7b02 | |||
| c42faa1e2e | |||
| 71f2967d4d | |||
| a5c72c49f1 | |||
| f58dd3d04f | |||
| 627e63e543 | |||
| 253f811125 | |||
| ad1e39268c | |||
| 3277e8fa32 | |||
| f1fb9e8ae9 | |||
| d96f25e843 | |||
| c5e72669c5 | |||
| 608bb94422 | |||
| ca0e764284 | |||
| fd4ab90dc9 | |||
| 9568e3fe26 | |||
| fe5816bc9e | |||
| 13659bee24 | |||
| fd9506fcb3 | |||
| ec2f33b283 | |||
| ecace7683d | |||
| 2e95ea8e94 | |||
| def7aad833 | |||
| c0f8dd792d | |||
| 09cfcdf61a | |||
| 0d0176aac2 | |||
| 6f297c4ebf | |||
| 8aa2f563a4 | |||
| 904bc688ca | |||
| f9815a0a3f | |||
| dcd0fabc1f | |||
| 7750025631 | |||
| 500db31142 | |||
| 2584b4f125 | |||
| 42867560c3 | |||
| b5f2a75b86 | |||
| 1b6c440dcc | |||
| b345137f16 | |||
| d87ddb5f6a | |||
| d842466594 | |||
| 34ea821fb3 | |||
| e957f1192a | |||
| d90b2750e0 | |||
| cea8083422 | |||
| 757fb1e454 | |||
| 0d51f3c373 | |||
| 176a3ed0a6 | |||
| a2e4157b9f | |||
| 080d98389d | |||
| d35b97fdec | |||
| 541429aabd | |||
| 39ed74996d | |||
| f870198011 | |||
| 12aee103df | |||
| 522af51a8d | |||
| edd803c304 | |||
| a5fcb0988b | |||
| 66bfe474c2 | |||
| f2b57dccc0 | |||
| c5e81ebcc6 | |||
| cf563571c8 | |||
| dae342b70e | |||
| d02550f5a0 | |||
| 87ceaa805b | |||
| f41d49bd26 | |||
| 58cd0b06e9 | |||
| d92da6afa4 | |||
| 85f4d47318 | |||
| 006499c2ee | |||
| 568901edb6 | |||
| 8bf9d1d7e2 | |||
| 79770dc7fa | |||
| 22f047bbe5 | |||
| 52c724eac6 | |||
| 8d9fe62c53 | |||
| 41b96e2ce5 | |||
| b8b4a33f9f | |||
| b937ed8051 | |||
| f49c4e9d37 | |||
| 7ebe600adf |
+11
-2
@@ -1,6 +1,9 @@
|
|||||||
.idea
|
.idea
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.log
|
||||||
.vs
|
.vs
|
||||||
*.DotSettings*
|
*.DotSettings*
|
||||||
*.user
|
*.user
|
||||||
@@ -11,6 +14,12 @@ venv
|
|||||||
*.c
|
*.c
|
||||||
*.pyd
|
*.pyd
|
||||||
cython_debug*
|
cython_debug*
|
||||||
|
dist-dlls
|
||||||
|
dist-azaion
|
||||||
|
Azaion*.exe
|
||||||
|
Azaion*.bin
|
||||||
|
|
||||||
|
azaion\.*\.big
|
||||||
|
_internal
|
||||||
dist
|
dist
|
||||||
AzaionSuiteInstaller.exe
|
*.jpg
|
||||||
azaion\.*\.big
|
|
||||||
|
|||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Azaion.Suite (with credentials)",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
|
||||||
|
"args": ["credsManual", "-e", "test-admin@azaion.com", "-p", "Az@1on1000TestT-addminn11"],
|
||||||
|
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Azaion.Suite",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build",
|
||||||
|
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to Azaion.Suite",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach",
|
||||||
|
"processName": "Azaion.Suite"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"${workspaceFolder}/Azaion.Suite.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/Azaion.Suite.sln",
|
||||||
|
"/property:GenerateFullPaths=true",
|
||||||
|
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/Azaion.Suite.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
<RowDefinition Height="28"></RowDefinition>
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
<RowDefinition Height="28"></RowDefinition>
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="80"></RowDefinition>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -177,6 +178,7 @@
|
|||||||
<ListView.ContextMenu>
|
<ListView.ContextMenu>
|
||||||
<ContextMenu Name="LvFilesContextMenu">
|
<ContextMenu Name="LvFilesContextMenu">
|
||||||
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
|
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
|
||||||
|
<MenuItem Header="Видалити..." Click="DeleteMedia" Background="WhiteSmoke" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</ListView.ContextMenu>
|
</ListView.ContextMenu>
|
||||||
<ListView.View>
|
<ListView.View>
|
||||||
@@ -190,10 +192,20 @@
|
|||||||
</GridView>
|
</GridView>
|
||||||
</ListView.View>
|
</ListView.View>
|
||||||
</ListView>
|
</ListView>
|
||||||
|
|
||||||
|
<controls1:CameraConfigControl
|
||||||
|
x:Name="CameraConfigControl"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="4"
|
||||||
|
Camera="{Binding Camera, RelativeSource={RelativeSource AncestorType=Window}, Mode=OneWay}"
|
||||||
|
>
|
||||||
|
|
||||||
|
</controls1:CameraConfigControl>
|
||||||
|
|
||||||
<controls1:DetectionClasses
|
<controls1:DetectionClasses
|
||||||
x:Name="LvClasses"
|
x:Name="LvClasses"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="4">
|
Grid.Row="5">
|
||||||
</controls1:DetectionClasses>
|
</controls1:DetectionClasses>
|
||||||
|
|
||||||
<GridSplitter
|
<GridSplitter
|
||||||
@@ -201,7 +213,7 @@
|
|||||||
ResizeDirection="Columns"
|
ResizeDirection="Columns"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.RowSpan="4"
|
Grid.RowSpan="5"
|
||||||
ResizeBehavior="PreviousAndNext"
|
ResizeBehavior="PreviousAndNext"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
@@ -210,7 +222,7 @@
|
|||||||
<wpf:VideoView
|
<wpf:VideoView
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Grid.RowSpan="4"
|
Grid.RowSpan="5"
|
||||||
x:Name="VideoView">
|
x:Name="VideoView">
|
||||||
<controls1:CanvasEditor x:Name="Editor"
|
<controls1:CanvasEditor x:Name="Editor"
|
||||||
Background="#01000000"
|
Background="#01000000"
|
||||||
@@ -223,7 +235,7 @@
|
|||||||
ResizeDirection="Columns"
|
ResizeDirection="Columns"
|
||||||
Grid.Column="3"
|
Grid.Column="3"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.RowSpan="4"
|
Grid.RowSpan="5"
|
||||||
ResizeBehavior="PreviousAndNext"
|
ResizeBehavior="PreviousAndNext"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
@@ -233,7 +245,7 @@
|
|||||||
<DataGrid x:Name="DgAnnotations"
|
<DataGrid x:Name="DgAnnotations"
|
||||||
Grid.Column="4"
|
Grid.Column="4"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.RowSpan="4"
|
Grid.RowSpan="5"
|
||||||
Background="Black"
|
Background="Black"
|
||||||
Foreground="White"
|
Foreground="White"
|
||||||
RowHeaderWidth="0"
|
RowHeaderWidth="0"
|
||||||
@@ -315,7 +327,7 @@
|
|||||||
<ColumnDefinition Width="28" /> <!-- 10 -->
|
<ColumnDefinition Width="28" /> <!-- 10 -->
|
||||||
<ColumnDefinition Width="28" /> <!-- 11 -->
|
<ColumnDefinition Width="28" /> <!-- 11 -->
|
||||||
<ColumnDefinition Width="28" /> <!-- 12 -->
|
<ColumnDefinition Width="28" /> <!-- 12 -->
|
||||||
<ColumnDefinition Width="0" /> <!-- 13 -->
|
<ColumnDefinition Width="28" /> <!-- 13 -->
|
||||||
<ColumnDefinition Width="*" /> <!-- 14-->
|
<ColumnDefinition Width="*" /> <!-- 14-->
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
|
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
|
||||||
@@ -496,11 +508,12 @@
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
x:Name="AIDetectBtn"
|
x:Name="AIDetectBtn"
|
||||||
|
IsEnabled="False"
|
||||||
Grid.Column="10"
|
Grid.Column="10"
|
||||||
Padding="2" Width="25"
|
Padding="2" Width="25"
|
||||||
Height="25"
|
Height="25"
|
||||||
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
|
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
|
||||||
Click="AutoDetect">
|
Click="AIDetectBtn_OnClick">
|
||||||
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
|
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
|
||||||
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
|
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
|
||||||
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
|
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
|
||||||
@@ -602,6 +615,19 @@
|
|||||||
</Image.Source>
|
</Image.Source>
|
||||||
</Image>
|
</Image>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Grid.Column="13"
|
||||||
|
Padding="2"
|
||||||
|
Width="25"
|
||||||
|
Height="25"
|
||||||
|
ToolTip="Аналіз стану БПЛА. Клавіша: [K]" Background="Black" BorderBrush="Black"
|
||||||
|
Click="RunDroneMaintenance">
|
||||||
|
<Path Stretch="Fill" Fill="LightGray" Data="
|
||||||
|
M128,7.10542736e-15 C198.692448,7.10542736e-15 256,57.307552 256,128 C256,140.931179 254.082471,153.414494 250.516246,165.181113 L384,298.666667
|
||||||
|
C407.564149,322.230816 407.564149,360.435851 384,384 C360.435851,407.564149 322.230816,407.564149 298.666667,384 L165.181113,250.516246
|
||||||
|
C153.414494,254.082471 140.931179,256 128,256 C57.307552,256 7.10542736e-15,198.692448 7.10542736e-15,128 C7.10542736e-15,114.357909
|
||||||
|
2.13416363,101.214278 6.08683609,88.884763 L66.6347809,149.333333 L126.649,129.346 L129.329,126.666 L149.333333,66.7080586 L88.7145729,6.14152881
|
||||||
|
C101.0933,2.15385405 114.29512,7.10542736e-15 128,7.10542736e-15 Z" />
|
||||||
|
</Button>
|
||||||
<StatusBar Grid.Column="14"
|
<StatusBar Grid.Column="14"
|
||||||
Background="#252525"
|
Background="#252525"
|
||||||
Foreground="White">
|
Foreground="White">
|
||||||
|
|||||||
+193
-243
@@ -14,6 +14,7 @@ using Azaion.Common.DTO.Config;
|
|||||||
using Azaion.Common.Events;
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.Common.Services.Inference;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
@@ -28,8 +29,8 @@ namespace Azaion.Annotator;
|
|||||||
|
|
||||||
public partial class Annotator
|
public partial class Annotator
|
||||||
{
|
{
|
||||||
private readonly AppConfig _appConfig;
|
private readonly AppConfig? _appConfig;
|
||||||
private readonly LibVLC _libVLC;
|
private readonly LibVLC _libVlc;
|
||||||
private readonly MediaPlayer _mediaPlayer;
|
private readonly MediaPlayer _mediaPlayer;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly FormState _formState;
|
private readonly FormState _formState;
|
||||||
@@ -37,62 +38,68 @@ public partial class Annotator
|
|||||||
private readonly IConfigUpdater _configUpdater;
|
private readonly IConfigUpdater _configUpdater;
|
||||||
private readonly HelpWindow _helpWindow;
|
private readonly HelpWindow _helpWindow;
|
||||||
private readonly ILogger<Annotator> _logger;
|
private readonly ILogger<Annotator> _logger;
|
||||||
private readonly AnnotationService _annotationService;
|
|
||||||
private readonly IDbFactory _dbFactory;
|
private readonly IDbFactory _dbFactory;
|
||||||
private readonly IInferenceService _inferenceService;
|
private readonly IInferenceService _inferenceService;
|
||||||
|
private readonly IInferenceClient _inferenceClient;
|
||||||
|
|
||||||
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
|
|
||||||
private bool _suspendLayout;
|
private bool _suspendLayout;
|
||||||
private bool _gpsPanelVisible = false;
|
private bool _gpsPanelVisible;
|
||||||
|
|
||||||
public readonly CancellationTokenSource MainCancellationSource = new();
|
private readonly CancellationTokenSource _mainCancellationSource = new();
|
||||||
public CancellationTokenSource DetectionCancellationSource = new();
|
public CancellationTokenSource DetCancelSource = new();
|
||||||
public bool FollowAI = false;
|
private bool _isInferenceNow;
|
||||||
public bool IsInferenceNow = false;
|
|
||||||
|
|
||||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
|
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
|
||||||
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
|
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
|
||||||
private readonly IGpsMatcherService _gpsMatcherService;
|
|
||||||
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
|
|
||||||
|
|
||||||
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
|
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
|
||||||
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
|
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
|
||||||
|
public Dictionary<string, MediaFileInfo> MediaFilesDict = new();
|
||||||
|
|
||||||
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
|
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
|
||||||
|
public string MainTitle { get; set; }
|
||||||
|
|
||||||
|
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
|
||||||
|
|
||||||
public Annotator(
|
public Annotator(
|
||||||
IConfigUpdater configUpdater,
|
IConfigUpdater configUpdater,
|
||||||
IOptions<AppConfig> appConfig,
|
IOptions<AppConfig> appConfig,
|
||||||
LibVLC libVLC,
|
LibVLC libVlc,
|
||||||
MediaPlayer mediaPlayer,
|
MediaPlayer mediaPlayer,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
FormState formState,
|
FormState formState,
|
||||||
HelpWindow helpWindow,
|
HelpWindow helpWindow,
|
||||||
ILogger<Annotator> logger,
|
ILogger<Annotator> logger,
|
||||||
AnnotationService annotationService,
|
|
||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
IInferenceService inferenceService,
|
IInferenceService inferenceService,
|
||||||
|
IInferenceClient inferenceClient,
|
||||||
IGpsMatcherService gpsMatcherService)
|
IGpsMatcherService gpsMatcherService)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
// Initialize configuration and services BEFORE InitializeComponent so bindings can see real values
|
||||||
|
|
||||||
_appConfig = appConfig.Value;
|
_appConfig = appConfig.Value;
|
||||||
_configUpdater = configUpdater;
|
_configUpdater = configUpdater;
|
||||||
_libVLC = libVLC;
|
_libVlc = libVlc;
|
||||||
_mediaPlayer = mediaPlayer;
|
_mediaPlayer = mediaPlayer;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_formState = formState;
|
_formState = formState;
|
||||||
_helpWindow = helpWindow;
|
_helpWindow = helpWindow;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_annotationService = annotationService;
|
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_inferenceService = inferenceService;
|
_inferenceService = inferenceService;
|
||||||
_gpsMatcherService = gpsMatcherService;
|
_inferenceClient = inferenceClient;
|
||||||
|
|
||||||
|
// Ensure bindings (e.g., Camera) resolve immediately
|
||||||
|
DataContext = this;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
MainTitle = $"Azaion Annotator {Constants.GetLocalVersion()}";
|
||||||
|
Title = MainTitle;
|
||||||
|
|
||||||
Loaded += OnLoaded;
|
Loaded += OnLoaded;
|
||||||
Closed += OnFormClosed;
|
Closed += OnFormClosed;
|
||||||
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
|
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
|
||||||
TbFolder.TextChanged += async (sender, args) =>
|
TbFolder.TextChanged += async (_, _) =>
|
||||||
{
|
{
|
||||||
if (!Path.Exists(TbFolder.Text))
|
if (!Path.Exists(TbFolder.Text))
|
||||||
return;
|
return;
|
||||||
@@ -100,16 +107,21 @@ public partial class Annotator
|
|||||||
{
|
{
|
||||||
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
|
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
|
||||||
await ReloadFiles();
|
await ReloadFiles();
|
||||||
await SaveUserSettings();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, e.Message);
|
_logger.LogError(e, e.Message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
||||||
MapMatcherComponent.Init(_appConfig, _gpsMatcherService);
|
MapMatcherComponent.Init(_appConfig, gpsMatcherService);
|
||||||
|
|
||||||
|
// When camera settings change, persist config
|
||||||
|
CameraConfigControl.CameraChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (_appConfig != null)
|
||||||
|
_configUpdater.Save(_appConfig);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
@@ -119,16 +131,13 @@ public partial class Annotator
|
|||||||
|
|
||||||
_suspendLayout = true;
|
_suspendLayout = true;
|
||||||
|
|
||||||
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.UIConfig.LeftPanelWidth);
|
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.LeftPanelWidth ?? Constants.DEFAULT_LEFT_PANEL_WIDTH);
|
||||||
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.UIConfig.RightPanelWidth);
|
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.RightPanelWidth ?? Constants.DEFAULT_RIGHT_PANEL_WIDTH);
|
||||||
|
|
||||||
_suspendLayout = false;
|
_suspendLayout = false;
|
||||||
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
|
TbFolder.Text = _appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR;
|
||||||
|
|
||||||
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
|
LvClasses.Init(_appConfig?.AnnotationConfig.DetectionClasses ?? Constants.DefaultAnnotationClasses);
|
||||||
|
|
||||||
if (LvFiles.Items.IsEmpty)
|
|
||||||
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BlinkHelp(string helpText, int times = 2)
|
public void BlinkHelp(string helpText, int times = 2)
|
||||||
@@ -152,31 +161,16 @@ public partial class Annotator
|
|||||||
VideoView.MediaPlayer = _mediaPlayer;
|
VideoView.MediaPlayer = _mediaPlayer;
|
||||||
|
|
||||||
//On start playing media
|
//On start playing media
|
||||||
_mediaPlayer.Playing += async (sender, args) =>
|
_mediaPlayer.Playing += (_, _) =>
|
||||||
{
|
{
|
||||||
if (_formState.CurrentMrl == _mediaPlayer.Media?.Mrl)
|
|
||||||
return; //already loaded all the info
|
|
||||||
|
|
||||||
_formState.CurrentMrl = _mediaPlayer.Media?.Mrl ?? "";
|
|
||||||
uint vw = 0, vh = 0;
|
uint vw = 0, vh = 0;
|
||||||
_mediaPlayer.Size(0, ref vw, ref vh);
|
_mediaPlayer.Size(0, ref vw, ref vh);
|
||||||
_formState.CurrentVideoSize = new Size(vw, vh);
|
_formState.CurrentMediaSize = new Size(vw, vh);
|
||||||
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
||||||
|
|
||||||
await Dispatcher.Invoke(async () => await ReloadAnnotations());
|
|
||||||
|
|
||||||
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
|
|
||||||
{
|
|
||||||
await Task.Delay(100); //wait to load the frame and set on pause
|
|
||||||
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
|
||||||
_mediaPlayer.SetPause(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LvFiles.MouseDoubleClick += async (_, _) =>
|
LvFiles.MouseDoubleClick += async (_, _) =>
|
||||||
{
|
{
|
||||||
if (IsInferenceNow)
|
|
||||||
FollowAI = false;
|
|
||||||
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
|
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,12 +179,12 @@ public partial class Annotator
|
|||||||
var selectedClass = args.DetectionClass;
|
var selectedClass = args.DetectionClass;
|
||||||
Editor.CurrentAnnClass = selectedClass;
|
Editor.CurrentAnnClass = selectedClass;
|
||||||
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
||||||
};
|
};
|
||||||
|
|
||||||
_mediaPlayer.PositionChanged += (o, args) =>
|
_mediaPlayer.PositionChanged += (_, _) =>
|
||||||
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
|
||||||
|
|
||||||
VideoSlider.ValueChanged += (value, newValue) =>
|
VideoSlider.ValueChanged += (_, newValue) =>
|
||||||
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
|
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
|
||||||
|
|
||||||
VideoSlider.KeyDown += (sender, args) =>
|
VideoSlider.KeyDown += (sender, args) =>
|
||||||
@@ -199,77 +193,64 @@ public partial class Annotator
|
|||||||
Volume.ValueChanged += (_, newValue) =>
|
Volume.ValueChanged += (_, newValue) =>
|
||||||
_mediator.Publish(new VolumeChangedEvent((int)newValue));
|
_mediator.Publish(new VolumeChangedEvent((int)newValue));
|
||||||
|
|
||||||
SizeChanged += async (_, _) => await SaveUserSettings();
|
|
||||||
LocationChanged += async (_, _) => await SaveUserSettings();
|
|
||||||
StateChanged += async (_, _) => await SaveUserSettings();
|
|
||||||
|
|
||||||
DgAnnotations.MouseDoubleClick += (sender, args) =>
|
DgAnnotations.MouseDoubleClick += (sender, args) =>
|
||||||
{
|
{
|
||||||
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
|
if (ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) is DataGridRow dgRow)
|
||||||
if (dgRow != null)
|
OpenAnnotationResult((Annotation)dgRow.Item);
|
||||||
OpenAnnotationResult((AnnotationResult)dgRow!.Item);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DgAnnotations.KeyUp += async (sender, args) =>
|
DgAnnotations.KeyUp += async (_, args) =>
|
||||||
{
|
{
|
||||||
switch (args.Key)
|
switch (args.Key)
|
||||||
{
|
{
|
||||||
case Key.Up:
|
|
||||||
case Key.Down: //cursor is already moved by system behaviour
|
case Key.Down: //cursor is already moved by system behaviour
|
||||||
OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem);
|
OpenAnnotationResult((Annotation)DgAnnotations.SelectedItem);
|
||||||
break;
|
break;
|
||||||
case Key.Delete:
|
case Key.Delete:
|
||||||
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||||
if (result != MessageBoxResult.OK)
|
if (result != MessageBoxResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
|
var res = DgAnnotations.SelectedItems.Cast<Annotation>().ToList();
|
||||||
var annotations = res.Select(x => x.Annotation).ToList();
|
var annotationNames = res.Select(x => x.Name).ToList();
|
||||||
|
|
||||||
await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
|
await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Editor.Mediator = _mediator;
|
|
||||||
DgAnnotations.ItemsSource = _formState.AnnotationResults;
|
DgAnnotations.ItemsSource = _formState.AnnotationResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenAnnotationResult(AnnotationResult res)
|
private void OpenAnnotationResult(Annotation ann)
|
||||||
{
|
{
|
||||||
if (IsInferenceNow)
|
|
||||||
FollowAI = false;
|
|
||||||
_mediaPlayer.SetPause(true);
|
_mediaPlayer.SetPause(true);
|
||||||
Editor.RemoveAllAnns();
|
if (!ann.IsSplit)
|
||||||
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
|
Editor.RemoveAllAnns();
|
||||||
|
|
||||||
|
_mediaPlayer.Time = (long)ann.Time.TotalMilliseconds;
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
||||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||||
Editor.ClearExpiredAnnotations(res.Annotation.Time);
|
Editor.ClearExpiredAnnotations(ann.Time);
|
||||||
});
|
});
|
||||||
|
|
||||||
ShowAnnotations(res.Annotation, showImage: true);
|
ShowAnnotation(ann, showImage: true, openResult: true);
|
||||||
}
|
}
|
||||||
private Task SaveUserSettings()
|
private void SaveUserSettings()
|
||||||
{
|
{
|
||||||
if (_suspendLayout)
|
if (_suspendLayout || _appConfig is null)
|
||||||
return Task.CompletedTask;
|
return;
|
||||||
|
|
||||||
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||||
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||||
|
|
||||||
ThrottleExt.Throttle(() =>
|
_configUpdater.Save(_appConfig);
|
||||||
{
|
|
||||||
_configUpdater.Save(_appConfig);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowTimeAnnotations(TimeSpan time)
|
public void ShowTimeAnnotations(TimeSpan time, bool showImage = false)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
@@ -277,49 +258,57 @@ public partial class Annotator
|
|||||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||||
Editor.ClearExpiredAnnotations(time);
|
Editor.ClearExpiredAnnotations(time);
|
||||||
});
|
});
|
||||||
|
var annotations = TimedAnnotations.Query(time).ToList();
|
||||||
ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault());
|
if (!annotations.Any())
|
||||||
|
return;
|
||||||
|
foreach (var ann in annotations)
|
||||||
|
ShowAnnotation(ann, showImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowAnnotations(Annotation? annotation, bool showImage = false)
|
private void ShowAnnotation(Annotation annotation, bool showImage = false, bool openResult = false)
|
||||||
{
|
{
|
||||||
if (annotation == null)
|
|
||||||
return;
|
|
||||||
Dispatcher.Invoke(async () =>
|
Dispatcher.Invoke(async () =>
|
||||||
{
|
{
|
||||||
var videoSize = _formState.CurrentVideoSize;
|
if (showImage && !annotation.IsSplit && File.Exists(annotation.ImagePath))
|
||||||
if (showImage)
|
|
||||||
{
|
{
|
||||||
if (File.Exists(annotation.ImagePath))
|
Editor.SetBackground(await annotation.ImagePath.OpenImage());
|
||||||
{
|
_formState.BackgroundTime = annotation.Time;
|
||||||
Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
|
|
||||||
_formState.BackgroundTime = annotation.Time;
|
|
||||||
videoSize = Editor.RenderSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Editor.CreateDetections(annotation.Time, annotation.Detections, _appConfig.AnnotationConfig.DetectionClasses, videoSize);
|
|
||||||
|
if (annotation.SplitTile != null && openResult)
|
||||||
|
{
|
||||||
|
var canvasTileLocation = new CanvasLabel(new YoloLabel(annotation.SplitTile, _formState.CurrentMediaSize),
|
||||||
|
RenderSize);
|
||||||
|
Editor.ZoomTo(new Point(canvasTileLocation.CenterX, canvasTileLocation.CenterY));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Editor.CreateDetections(annotation, _appConfig?.AnnotationConfig.DetectionClasses ?? [], _formState.CurrentMediaSize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadAnnotations()
|
public async Task ReloadAnnotations()
|
||||||
{
|
{
|
||||||
_formState.AnnotationResults.Clear();
|
await Dispatcher.InvokeAsync(async () =>
|
||||||
TimedAnnotations.Clear();
|
|
||||||
Editor.RemoveAllAnns();
|
|
||||||
|
|
||||||
var annotations = await _dbFactory.Run(async db =>
|
|
||||||
await db.Annotations.LoadWith(x => x.Detections)
|
|
||||||
.Where(x => x.OriginalMediaName == _formState.VideoName)
|
|
||||||
.OrderBy(x => x.Time)
|
|
||||||
.ToListAsync(token: MainCancellationSource.Token));
|
|
||||||
|
|
||||||
TimedAnnotations.Clear();
|
|
||||||
_formState.AnnotationResults.Clear();
|
|
||||||
foreach (var ann in annotations)
|
|
||||||
{
|
{
|
||||||
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
|
_formState.AnnotationResults.Clear();
|
||||||
_formState.AnnotationResults.Add(new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, ann));
|
TimedAnnotations.Clear();
|
||||||
}
|
Editor.RemoveAllAnns();
|
||||||
|
|
||||||
|
var annotations = await _dbFactory.Run(async db =>
|
||||||
|
await db.Annotations.LoadWith(x => x.Detections)
|
||||||
|
.Where(x => x.OriginalMediaName == _formState.MediaName)
|
||||||
|
.OrderBy(x => x.Time)
|
||||||
|
.ToListAsync(token: _mainCancellationSource.Token));
|
||||||
|
|
||||||
|
TimedAnnotations.Clear();
|
||||||
|
_formState.AnnotationResults.Clear();
|
||||||
|
foreach (var ann in annotations)
|
||||||
|
{
|
||||||
|
// Duplicate for speed
|
||||||
|
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
|
||||||
|
_formState.AnnotationResults.Add(ann);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add manually
|
//Add manually
|
||||||
@@ -330,7 +319,7 @@ public partial class Annotator
|
|||||||
TimedAnnotations.Remove(previousAnnotations);
|
TimedAnnotations.Remove(previousAnnotations);
|
||||||
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
|
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
|
||||||
|
|
||||||
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time);
|
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
|
||||||
if (existingResult != null)
|
if (existingResult != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -342,32 +331,29 @@ public partial class Annotator
|
|||||||
_logger.LogError(e, e.Message);
|
_logger.LogError(e, e.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var dict = _formState.AnnotationResults
|
var dict = _formState.AnnotationResults
|
||||||
.Select((x, i) => new { x.Annotation.Time, Index = i })
|
.Select((x, i) => new { x.Time, Index = i })
|
||||||
.ToDictionary(x => x.Time, x => x.Index);
|
.ToDictionary(x => x.Time, x => x.Index);
|
||||||
|
|
||||||
var index = dict.Where(x => x.Key < time)
|
var index = dict.Where(x => x.Key < time)
|
||||||
.OrderBy(x => time - x.Key)
|
.OrderBy(x => time - x.Key)
|
||||||
.Select(x => x.Value + 1)
|
.Select(x => x.Value + 1)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
_formState.AnnotationResults.Insert(index, annotation);
|
||||||
var annRes = new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation);
|
|
||||||
_formState.AnnotationResults.Insert(index, annRes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadFiles()
|
private async Task ReloadFiles()
|
||||||
{
|
{
|
||||||
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
|
var dir = new DirectoryInfo(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR);
|
||||||
if (!dir.Exists)
|
if (!dir.Exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
|
var videoFiles = dir.GetFiles((_appConfig?.AnnotationConfig.VideoFormats ?? Constants.DefaultVideoFormats)
|
||||||
|
.ToArray()).Select(x =>
|
||||||
{
|
{
|
||||||
using var media = new Media(_libVLC, x.FullName);
|
var media = new Media(_libVlc, x.FullName);
|
||||||
media.Parse();
|
media.Parse();
|
||||||
var fInfo = new MediaFileInfo
|
var fInfo = new MediaFileInfo
|
||||||
{
|
{
|
||||||
@@ -379,7 +365,7 @@ public partial class Annotator
|
|||||||
return fInfo;
|
return fInfo;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
|
var imageFiles = dir.GetFiles((_appConfig?.AnnotationConfig.ImageFormats ?? Constants.DefaultImageFormats).ToArray())
|
||||||
.Select(x => new MediaFileInfo
|
.Select(x => new MediaFileInfo
|
||||||
{
|
{
|
||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
@@ -390,32 +376,32 @@ public partial class Annotator
|
|||||||
|
|
||||||
var allFileNames = allFiles.Select(x => x.FName).ToList();
|
var allFileNames = allFiles.Select(x => x.FName).ToList();
|
||||||
|
|
||||||
var labelsDict = await _dbFactory.Run(async db => await db.Annotations
|
var labelsDict = await _dbFactory.Run(async db =>
|
||||||
.GroupBy(x => x.Name.Substring(0, x.Name.Length - 7))
|
await db.Annotations
|
||||||
|
.GroupBy(x => x.OriginalMediaName)
|
||||||
.Where(x => allFileNames.Contains(x.Key))
|
.Where(x => allFileNames.Contains(x.Key))
|
||||||
.ToDictionaryAsync(x => x.Key, x => x.Key));
|
.Select(x => x.Key)
|
||||||
|
.ToDictionaryAsync(x => x, x => x));
|
||||||
|
|
||||||
foreach (var mediaFile in allFiles)
|
foreach (var mediaFile in allFiles)
|
||||||
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
|
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
|
||||||
|
|
||||||
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
|
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
|
||||||
|
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
|
||||||
|
.ToDictionary(gr => gr.Key, gr => gr.First());
|
||||||
LvFiles.ItemsSource = AllMediaFiles;
|
LvFiles.ItemsSource = AllMediaFiles;
|
||||||
|
|
||||||
BlinkHelp(AllMediaFiles.Count == 0
|
|
||||||
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
|
||||||
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFormClosed(object? sender, EventArgs e)
|
private void OnFormClosed(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
MainCancellationSource.Cancel();
|
_mainCancellationSource.Cancel();
|
||||||
_inferenceService.StopInference();
|
_inferenceService.StopInference();
|
||||||
DetectionCancellationSource.Cancel();
|
DetCancelSource.Cancel();
|
||||||
|
|
||||||
_mediaPlayer.Stop();
|
_mediaPlayer.Stop();
|
||||||
_mediaPlayer.Dispose();
|
_mediaPlayer.Dispose();
|
||||||
_libVLC.Dispose();
|
_libVlc.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenContainingFolder(object sender, RoutedEventArgs e)
|
private void OpenContainingFolder(object sender, RoutedEventArgs e)
|
||||||
@@ -436,41 +422,38 @@ public partial class Annotator
|
|||||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SeekTo(TimeSpan time) =>
|
private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||||
SeekTo((long)time.TotalMilliseconds);
|
private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder();
|
||||||
|
|
||||||
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
private void OpenFolder()
|
||||||
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
|
||||||
|
|
||||||
private async Task OpenFolder()
|
|
||||||
{
|
{
|
||||||
var dlg = new CommonOpenFileDialog
|
var dlg = new CommonOpenFileDialog
|
||||||
{
|
{
|
||||||
Title = "Open Video folder",
|
Title = "Open Video folder",
|
||||||
IsFolderPicker = true,
|
IsFolderPicker = true,
|
||||||
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
|
InitialDirectory = Path.GetDirectoryName(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR)
|
||||||
};
|
};
|
||||||
var dialogResult = dlg.ShowDialog();
|
var dialogResult = dlg.ShowDialog();
|
||||||
|
|
||||||
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
if (_appConfig is not null)
|
||||||
|
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
||||||
|
|
||||||
TbFolder.Text = dlg.FileName;
|
TbFolder.Text = dlg.FileName;
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
|
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
|
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
|
||||||
|
MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.FName);
|
||||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlayClick(object sender, RoutedEventArgs e)
|
private void PlayClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (IsInferenceNow)
|
|
||||||
FollowAI = false;
|
|
||||||
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
|
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +476,7 @@ public partial class Annotator
|
|||||||
_helpWindow.Activate();
|
_helpWindow.Activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
|
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings();
|
||||||
|
|
||||||
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
|
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -501,112 +484,48 @@ public partial class Annotator
|
|||||||
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AutoDetect(object sender, RoutedEventArgs e)
|
private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (IsInferenceNow)
|
try
|
||||||
{
|
{
|
||||||
FollowAI = true;
|
await AutoDetect();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AutoDetect()
|
||||||
|
{
|
||||||
|
if (_isInferenceNow)
|
||||||
|
return;
|
||||||
|
|
||||||
if (LvFiles.Items.IsEmpty)
|
if (LvFiles.Items.IsEmpty)
|
||||||
return;
|
return;
|
||||||
if (LvFiles.SelectedIndex == -1)
|
if (LvFiles.SelectedIndex == -1)
|
||||||
LvFiles.SelectedIndex = 0;
|
LvFiles.SelectedIndex = 0;
|
||||||
|
|
||||||
Dispatcher.Invoke(() => Editor.ResetBackground());
|
Dispatcher.Invoke(() => Editor.SetBackground(null));
|
||||||
|
|
||||||
IsInferenceNow = true;
|
_isInferenceNow = true;
|
||||||
FollowAI = true;
|
AIDetectBtn.IsEnabled = false;
|
||||||
DetectionCancellationSource = new CancellationTokenSource();
|
|
||||||
var detectToken = DetectionCancellationSource.Token;
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
while (!detectToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var files = new List<string>();
|
|
||||||
await Dispatcher.Invoke(async () =>
|
|
||||||
{
|
|
||||||
//Take all medias
|
|
||||||
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
|
|
||||||
//.Where(x => !x.HasAnnotations)
|
|
||||||
.Take(Constants.DETECTION_BATCH_SIZE)
|
|
||||||
.Select(x => x.Path)
|
|
||||||
.ToList() ?? [];
|
|
||||||
if (files.Count != 0)
|
|
||||||
{
|
|
||||||
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
|
|
||||||
await ReloadAnnotations();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (files.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken);
|
DetCancelSource = new CancellationTokenSource();
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles)
|
||||||
{
|
.Skip(LvFiles.SelectedIndex)
|
||||||
if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
|
.Select(x => x.Path)
|
||||||
DetectionCancellationSource.Cancel();
|
.ToList();
|
||||||
LvFiles.SelectedIndex += files.Count;
|
if (files.Count == 0)
|
||||||
});
|
return;
|
||||||
}
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
LvFiles.Items.Refresh();
|
|
||||||
IsInferenceNow = false;
|
|
||||||
FollowAI = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
|
await _inferenceService.RunInference(files, _appConfig?.CameraConfig ?? Constants.DefaultCameraConfig, DetCancelSource.Token);
|
||||||
{
|
|
||||||
await Dispatcher.Invoke(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
|
|
||||||
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
|
|
||||||
{
|
|
||||||
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
|
|
||||||
.Select((info, i) => new
|
|
||||||
{
|
|
||||||
MediaInfo = info,
|
|
||||||
Index = i
|
|
||||||
})
|
|
||||||
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
|
|
||||||
if (nextFile != null)
|
|
||||||
{
|
|
||||||
LvFiles.SelectedIndex = nextFile.Index;
|
|
||||||
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddAnnotation(annotation);
|
LvFiles.Items.Refresh();
|
||||||
|
_isInferenceNow = false;
|
||||||
if (FollowAI)
|
StatusHelp.Text = "Розпізнавання завершено";
|
||||||
SeekTo(annotationImage.Milliseconds, false);
|
AIDetectBtn.IsEnabled = true;
|
||||||
|
|
||||||
var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
|
|
||||||
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
|
|
||||||
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
|
||||||
$"size=({det.Width:F2}, {det.Height:F2}), " +
|
|
||||||
$"conf: {det.Confidence*100:F0}%"));
|
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
if (_formState.CurrentMedia != null)
|
|
||||||
_formState.CurrentMedia.HasAnnotations = true;
|
|
||||||
LvFiles.Items.Refresh();
|
|
||||||
StatusHelp.Text = log;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, e.Message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
|
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
|
||||||
@@ -631,9 +550,40 @@ public partial class Annotator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Denys Wishes
|
||||||
|
|
||||||
private void SoundDetections(object sender, RoutedEventArgs e)
|
private void SoundDetections(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
MessageBox.Show("Функція Аудіоаналіз знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunDroneMaintenance(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Функція Аналіз стану БПЛА знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void DeleteMedia(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo;
|
||||||
|
if (mediaFileInfo == null)
|
||||||
|
return;
|
||||||
|
DeleteMedia(mediaFileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteMedia(MediaFileInfo mediaFileInfo)
|
||||||
|
{
|
||||||
|
var obj = mediaFileInfo.MediaType == MediaTypes.Image
|
||||||
|
? "цю картинку "
|
||||||
|
: "це відео ";
|
||||||
|
var result = MessageBox.Show($"Видалити {obj}?",
|
||||||
|
"Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
if (result != MessageBoxResult.Yes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
File.Delete(mediaFileInfo.Path);
|
||||||
|
AllMediaFiles.Remove(mediaFileInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,7 +591,7 @@ public class GradientStyleSelector : StyleSelector
|
|||||||
{
|
{
|
||||||
public override Style? SelectStyle(object item, DependencyObject container)
|
public override Style? SelectStyle(object item, DependencyObject container)
|
||||||
{
|
{
|
||||||
if (container is not DataGridRow row || row.DataContext is not AnnotationResult result)
|
if (container is not DataGridRow row || row.DataContext is not Annotation result)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var style = new Style(typeof(DataGridRow));
|
var style = new Style(typeof(DataGridRow));
|
||||||
@@ -671,7 +621,7 @@ public class GradientStyleSelector : StyleSelector
|
|||||||
foreach (var gradientStop in gradients)
|
foreach (var gradientStop in gradients)
|
||||||
brush.GradientStops.Add(gradientStop);
|
brush.GradientStops.Add(gradientStop);
|
||||||
|
|
||||||
style.Setters.Add(new Setter(DataGridRow.BackgroundProperty, brush));
|
style.Setters.Add(new Setter(Control.BackgroundProperty, brush));
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
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 System.Windows.Media.Imaging;
|
||||||
|
using Azaion.Annotator.Controls;
|
||||||
using Azaion.Annotator.DTO;
|
using Azaion.Annotator.DTO;
|
||||||
using Azaion.Common;
|
using Azaion.Common;
|
||||||
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.Events;
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Services.Inference;
|
||||||
|
using GMap.NET;
|
||||||
|
using GMap.NET.WindowsPresentation;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,25 +25,33 @@ using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
|||||||
namespace Azaion.Annotator;
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
public class AnnotatorEventHandler(
|
public class AnnotatorEventHandler(
|
||||||
LibVLC libVLC,
|
LibVLC libVlc,
|
||||||
MediaPlayer mediaPlayer,
|
MediaPlayer mediaPlayer,
|
||||||
Annotator mainWindow,
|
Annotator mainWindow,
|
||||||
FormState formState,
|
FormState formState,
|
||||||
AnnotationService annotationService,
|
IAnnotationService annotationService,
|
||||||
ILogger<AnnotatorEventHandler> logger,
|
ILogger<AnnotatorEventHandler> logger,
|
||||||
IOptions<DirectoriesConfig> dirConfig,
|
IOptions<DirectoriesConfig> dirConfig,
|
||||||
IInferenceService inferenceService)
|
IOptions<AnnotationConfig> annotationConfig,
|
||||||
|
IInferenceService inferenceService,
|
||||||
|
IDbFactory dbFactory,
|
||||||
|
IAzaionApi api,
|
||||||
|
FailsafeAnnotationsProducer producer)
|
||||||
:
|
:
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<AnnClassSelectedEvent>,
|
INotificationHandler<AnnClassSelectedEvent>,
|
||||||
INotificationHandler<AnnotatorControlEvent>,
|
INotificationHandler<AnnotatorControlEvent>,
|
||||||
INotificationHandler<VolumeChangedEvent>,
|
INotificationHandler<VolumeChangedEvent>,
|
||||||
INotificationHandler<AnnotationsDeletedEvent>
|
INotificationHandler<AnnotationsDeletedEvent>,
|
||||||
|
INotificationHandler<AnnotationAddedEvent>,
|
||||||
|
INotificationHandler<SetStatusTextEvent>,
|
||||||
|
INotificationHandler<GPSMatcherResultProcessedEvent>,
|
||||||
|
INotificationHandler<AIAvailabilityStatusEvent>
|
||||||
{
|
{
|
||||||
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 readonly string _tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
|
||||||
|
|
||||||
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
||||||
{
|
{
|
||||||
{ Key.Space, PlaybackControlEnum.Pause },
|
{ Key.Space, PlaybackControlEnum.Pause },
|
||||||
@@ -48,7 +64,7 @@ public class AnnotatorEventHandler(
|
|||||||
{ Key.PageDown, PlaybackControlEnum.Next },
|
{ Key.PageDown, PlaybackControlEnum.Next },
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
|
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct)
|
||||||
{
|
{
|
||||||
SelectClass(notification.DetectionClass);
|
SelectClass(notification.DetectionClass);
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -62,7 +78,7 @@ public class AnnotatorEventHandler(
|
|||||||
mainWindow.LvClasses.SelectNum(detClass.Id);
|
mainWindow.LvClasses.SelectNum(detClass.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default)
|
public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
if (keyEvent.WindowEnum != WindowEnum.Annotator)
|
||||||
return;
|
return;
|
||||||
@@ -78,19 +94,19 @@ public class AnnotatorEventHandler(
|
|||||||
SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
|
SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
|
||||||
|
|
||||||
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
if (_keysControlEnumDict.TryGetValue(key, out var value))
|
||||||
await ControlPlayback(value, cancellationToken);
|
await ControlPlayback(value, ct);
|
||||||
|
|
||||||
if (key == Key.R)
|
if (key == Key.R)
|
||||||
mainWindow.AutoDetect(null!, null!);
|
await mainWindow.AutoDetect();
|
||||||
|
|
||||||
#region Volume
|
#region Volume
|
||||||
switch (key)
|
switch (key)
|
||||||
{
|
{
|
||||||
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
case Key.VolumeMute when mediaPlayer.Volume == 0:
|
||||||
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken);
|
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
|
||||||
break;
|
break;
|
||||||
case Key.VolumeMute:
|
case Key.VolumeMute:
|
||||||
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken);
|
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
|
||||||
break;
|
break;
|
||||||
case Key.Up:
|
case Key.Up:
|
||||||
case Key.VolumeUp:
|
case Key.VolumeUp:
|
||||||
@@ -108,9 +124,9 @@ public class AnnotatorEventHandler(
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default)
|
public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await ControlPlayback(notification.PlaybackControl, cancellationToken);
|
await ControlPlayback(notification.PlaybackControl, ct);
|
||||||
mainWindow.VideoView.Focus();
|
mainWindow.VideoView.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,21 +143,26 @@ public class AnnotatorEventHandler(
|
|||||||
await Play(cancellationToken);
|
await Play(cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Pause:
|
case PlaybackControlEnum.Pause:
|
||||||
mediaPlayer.Pause();
|
if (mediaPlayer.IsPlaying)
|
||||||
if (mainWindow.IsInferenceNow)
|
|
||||||
mainWindow.FollowAI = false;
|
|
||||||
if (!mediaPlayer.IsPlaying)
|
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
|
||||||
|
|
||||||
if (formState.BackgroundTime.HasValue)
|
|
||||||
{
|
{
|
||||||
mainWindow.Editor.ResetBackground();
|
mediaPlayer.Pause();
|
||||||
formState.BackgroundTime = null;
|
mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
|
||||||
|
mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
|
||||||
|
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mediaPlayer.Play();
|
||||||
|
if (formState.BackgroundTime.HasValue)
|
||||||
|
{
|
||||||
|
mainWindow.Editor.SetBackground(null);
|
||||||
|
formState.BackgroundTime = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Stop:
|
case PlaybackControlEnum.Stop:
|
||||||
inferenceService.StopInference();
|
inferenceService.StopInference();
|
||||||
await mainWindow.DetectionCancellationSource.CancelAsync();
|
await mainWindow.DetCancelSource.CancelAsync();
|
||||||
mediaPlayer.Stop();
|
mediaPlayer.Stop();
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.PreviousFrame:
|
case PlaybackControlEnum.PreviousFrame:
|
||||||
@@ -151,11 +172,18 @@ public class AnnotatorEventHandler(
|
|||||||
mainWindow.SeekTo(mediaPlayer.Time + step);
|
mainWindow.SeekTo(mediaPlayer.Time + step);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.SaveAnnotations:
|
case PlaybackControlEnum.SaveAnnotations:
|
||||||
await SaveAnnotations(cancellationToken);
|
await SaveAnnotation(cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||||
|
var focusedElement = FocusManager.GetFocusedElement(mainWindow);
|
||||||
mainWindow.Editor.RemoveSelectedAnns();
|
if (focusedElement is ListViewItem item)
|
||||||
|
{
|
||||||
|
if (item.DataContext is not MediaFileInfo mediaFileInfo)
|
||||||
|
return;
|
||||||
|
mainWindow.DeleteMedia(mediaFileInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
mainWindow.Editor.RemoveSelectedAnns();
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.RemoveAllAnns:
|
case PlaybackControlEnum.RemoveAllAnns:
|
||||||
mainWindow.Editor.RemoveAllAnns();
|
mainWindow.Editor.RemoveAllAnns();
|
||||||
@@ -201,7 +229,7 @@ public class AnnotatorEventHandler(
|
|||||||
await Play(ct);
|
await Play(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
|
public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ChangeVolume(notification.Volume);
|
ChangeVolume(notification.Volume);
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -218,87 +246,254 @@ public class AnnotatorEventHandler(
|
|||||||
if (mainWindow.LvFiles.SelectedItem == null)
|
if (mainWindow.LvFiles.SelectedItem == null)
|
||||||
return;
|
return;
|
||||||
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
|
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
|
||||||
mainWindow.Editor.ResetBackground();
|
|
||||||
|
if (formState.CurrentMedia == mediaInfo)
|
||||||
|
return; //already loaded
|
||||||
|
|
||||||
formState.CurrentMedia = mediaInfo;
|
formState.CurrentMedia = mediaInfo;
|
||||||
//need to wait a bit for correct VLC playback event handling
|
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
|
||||||
await Task.Delay(100, ct);
|
|
||||||
mediaPlayer.Stop();
|
await mainWindow.ReloadAnnotations();
|
||||||
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
|
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
|
if (mediaInfo.MediaType == MediaTypes.Video)
|
||||||
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
|
{
|
||||||
if (formState.CurrentMedia.MediaType == MediaTypes.Image)
|
mainWindow.Editor.SetBackground(null);
|
||||||
mediaPlayer.SetPause(true);
|
//need to wait a bit for correct VLC playback event handling
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
mediaPlayer.Stop();
|
||||||
|
mediaPlayer.Play(new Media(libVlc, mediaInfo.Path));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
formState.BackgroundTime = TimeSpan.Zero;
|
||||||
|
var image = await mediaInfo.Path.OpenImage();
|
||||||
|
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
|
||||||
|
mainWindow.Editor.SetBackground(image);
|
||||||
|
mediaPlayer.Stop();
|
||||||
|
mainWindow.ShowTimeAnnotations(TimeSpan.Zero, showImage: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//SAVE: MANUAL
|
//SAVE: MANUAL
|
||||||
private async Task SaveAnnotations(CancellationToken cancellationToken = default)
|
private async Task SaveAnnotation(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (formState.CurrentMedia == null)
|
if (formState.CurrentMedia == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
var originalMediaName = formState.VideoName;
|
var timeName = formState.MediaName.ToTimeName(time);
|
||||||
var fName = originalMediaName.ToTimeName(time);
|
|
||||||
|
|
||||||
var currentDetections = mainWindow.Editor.CurrentDetections
|
|
||||||
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
|
|
||||||
mainWindow.LvFiles.Items.Refresh();
|
|
||||||
mainWindow.Editor.RemoveAllAnns();
|
|
||||||
|
|
||||||
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
||||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{Constants.JPG_EXT}");
|
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
|
||||||
|
|
||||||
if (formState.BackgroundTime.HasValue)
|
formState.CurrentMedia.HasAnnotations = mainWindow.Editor.CurrentDetections.Count != 0;
|
||||||
|
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
|
||||||
|
if (isVideo)
|
||||||
{
|
{
|
||||||
//no need to save image, it's already there, just remove background
|
foreach (var annotation in annotations)
|
||||||
mainWindow.Editor.ResetBackground();
|
mainWindow.AddAnnotation(annotation);
|
||||||
|
mediaPlayer.Play();
|
||||||
|
|
||||||
|
// next item. Probably not needed
|
||||||
|
// var annGrid = mainWindow.DgAnnotations;
|
||||||
|
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
||||||
|
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
||||||
|
|
||||||
|
mainWindow.Editor.SetBackground(null);
|
||||||
formState.BackgroundTime = null;
|
formState.BackgroundTime = null;
|
||||||
|
|
||||||
//next item
|
|
||||||
var annGrid = mainWindow.DgAnnotations;
|
|
||||||
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
|
||||||
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
|
await NextMedia(ct: cancellationToken);
|
||||||
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
|
|
||||||
if (isVideo)
|
|
||||||
mediaPlayer.Play();
|
|
||||||
else
|
|
||||||
await NextMedia(ct: cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
if (isVideo)
|
mainWindow.Editor.RemoveAllAnns();
|
||||||
mainWindow.AddAnnotation(annotation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
|
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x);
|
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
|
||||||
foreach (var ann in notification.Annotations)
|
|
||||||
{
|
|
||||||
if (!annResDict.TryGetValue(ann.Name, out var value))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
formState.AnnotationResults.Remove(value);
|
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
|
||||||
mainWindow.TimedAnnotations.Remove(ann);
|
var mediaSize = new Size(source.PixelWidth, source.PixelHeight);
|
||||||
}
|
var annotationsResult = new List<Annotation>();
|
||||||
|
if (!File.Exists(imgPath))
|
||||||
if (formState.AnnotationResults.Count == 0)
|
|
||||||
{
|
{
|
||||||
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
|
if (mediaSize.FitSizeForAI())
|
||||||
if (media != null)
|
await source.SaveImage(imgPath, cancellationToken);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
media.HasAnnotations = false;
|
//Tiling
|
||||||
mainWindow.LvFiles.Items.Refresh();
|
|
||||||
|
//1. Convert from RenderSize to CurrentMediaSize
|
||||||
|
var detectionCoords = canvasDetections.Select(x => new CanvasLabel(
|
||||||
|
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
//2. Split to frames
|
||||||
|
var results = TileProcessor.Split(formState.CurrentMediaSize, detectionCoords, cancellationToken);
|
||||||
|
|
||||||
|
//3. Save each frame as a separate annotation
|
||||||
|
foreach (var res in results)
|
||||||
|
{
|
||||||
|
var time = TimeSpan.Zero;
|
||||||
|
var annotationName = $"{formState.MediaName}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
|
||||||
|
|
||||||
|
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
|
||||||
|
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
|
||||||
|
await bitmap.SaveImage(tileImgPath, cancellationToken);
|
||||||
|
|
||||||
|
var frameSize = new Size(res.Tile.Width, res.Tile.Height);
|
||||||
|
var detections = res.Detections
|
||||||
|
.Select(det => det.ReframeToSmall(res.Tile))
|
||||||
|
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
annotationsResult.Add(await annotationService.SaveAnnotation(formState.MediaName, annotationName, time, detections, token: cancellationToken));
|
||||||
|
}
|
||||||
|
return annotationsResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Task.CompletedTask;
|
|
||||||
|
var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
|
var annName = formState.MediaName.ToTimeName(timeImg);
|
||||||
|
var currentDetections = canvasDetections.Select(x =>
|
||||||
|
new Detection(annName, new YoloLabel(x, mainWindow.Editor.RenderSize, mediaSize)))
|
||||||
|
.ToList();
|
||||||
|
var annotation = await annotationService.SaveAnnotation(formState.MediaName, annName, timeImg, currentDetections, token: cancellationToken);
|
||||||
|
return [annotation];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mainWindow.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var namesSet = notification.AnnotationNames.ToHashSet();
|
||||||
|
|
||||||
|
var remainAnnotations = formState.AnnotationResults
|
||||||
|
.Where(x => !namesSet.Contains(x.Name)).ToList();
|
||||||
|
formState.AnnotationResults.Clear();
|
||||||
|
foreach (var ann in remainAnnotations)
|
||||||
|
formState.AnnotationResults.Add(ann);
|
||||||
|
|
||||||
|
var timedAnnotationsToRemove = mainWindow.TimedAnnotations
|
||||||
|
.Where(x => namesSet.Contains(x.Value.Name))
|
||||||
|
.Select(x => x.Value).ToList();
|
||||||
|
mainWindow.TimedAnnotations.Remove(timedAnnotationsToRemove);
|
||||||
|
|
||||||
|
if (formState.AnnotationResults.Count == 0)
|
||||||
|
{
|
||||||
|
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
|
||||||
|
if (media != null)
|
||||||
|
{
|
||||||
|
media.HasAnnotations = false;
|
||||||
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
|
||||||
|
|
||||||
|
foreach (var name in notification.AnnotationNames)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
|
||||||
|
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
|
||||||
|
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
|
||||||
|
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Only validators can send Delete to the queue
|
||||||
|
if (!notification.FromQueue && api.CurrentUser.Role.IsValidator())
|
||||||
|
await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
|
||||||
|
{
|
||||||
|
mainWindow.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
|
||||||
|
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
|
||||||
|
if ((mediaInfo?.FName ?? "") == e.Annotation.OriginalMediaName)
|
||||||
|
mainWindow.AddAnnotation(e.Annotation);
|
||||||
|
|
||||||
|
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
|
||||||
|
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
|
||||||
|
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
||||||
|
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
|
||||||
|
$"conf: {det.Confidence*100:F0}%"));
|
||||||
|
|
||||||
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
|
|
||||||
|
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
|
||||||
|
if (media != null)
|
||||||
|
media.HasAnnotations = true;
|
||||||
|
|
||||||
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
|
mainWindow.StatusHelp.Text = log;
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
mainWindow.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
mainWindow.StatusHelp.Text = e.Text;
|
||||||
|
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
mainWindow.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
|
||||||
|
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
|
||||||
|
if (e.ProcessedGeoPoint != e.GeoPoint)
|
||||||
|
AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet);
|
||||||
|
ann.Lat = e.GeoPoint.Lat;
|
||||||
|
ann.Lon = e.GeoPoint.Lon;
|
||||||
|
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMarker(GeoPoint point, string text, SolidColorBrush color)
|
||||||
|
{
|
||||||
|
var map = mainWindow.MapMatcherComponent;
|
||||||
|
var pointLatLon = new PointLatLng(point.Lat, point.Lon);
|
||||||
|
var marker = new GMapMarker(pointLatLon);
|
||||||
|
marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color);
|
||||||
|
map.SatelliteMap.Markers.Add(marker);
|
||||||
|
map.SatelliteMap.Position = pointLatLon;
|
||||||
|
map.SatelliteMap.ZoomAndCenterMarkers(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
mainWindow.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
logger.LogInformation(e.ToString());
|
||||||
|
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
|
||||||
|
mainWindow.StatusHelp.Text = e.ToString();
|
||||||
|
});
|
||||||
|
if (e.Status is AIAvailabilityEnum.Enabled or AIAvailabilityEnum.Error)
|
||||||
|
await inferenceService.CheckAIAvailabilityTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,24 +7,27 @@
|
|||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
|
||||||
|
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
|
||||||
|
|
||||||
|
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
|
||||||
|
<FileVersion>$(AssemblyVersion)</FileVersion>
|
||||||
|
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
|
||||||
|
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
|
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
|
||||||
<PackageReference Include="libc.translation" Version="7.1.1" />
|
<PackageReference Include="libc.translation" Version="7.1.1" />
|
||||||
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
|
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
|
||||||
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
|
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
|
||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="RangeTree" Version="3.0.1" />
|
<PackageReference Include="RangeTree" Version="3.0.1" />
|
||||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
||||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||||
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
|
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Azaion.Annotator.Controls
|
|||||||
{
|
{
|
||||||
public readonly GMapMarker Marker;
|
public readonly GMapMarker Marker;
|
||||||
|
|
||||||
public CircleVisual(GMapMarker m, Brush background)
|
public CircleVisual(GMapMarker m, int size, string text, Brush background)
|
||||||
{
|
{
|
||||||
ShadowEffect = new DropShadowEffect();
|
ShadowEffect = new DropShadowEffect();
|
||||||
Marker = m;
|
Marker = m;
|
||||||
@@ -22,14 +22,14 @@ namespace Azaion.Annotator.Controls
|
|||||||
MouseLeave += CircleVisual_MouseLeave;
|
MouseLeave += CircleVisual_MouseLeave;
|
||||||
Loaded += OnLoaded;
|
Loaded += OnLoaded;
|
||||||
|
|
||||||
Text = "?";
|
Text = text;
|
||||||
|
|
||||||
StrokeArrow.EndLineCap = PenLineCap.Triangle;
|
StrokeArrow.EndLineCap = PenLineCap.Triangle;
|
||||||
StrokeArrow.LineJoin = PenLineJoin.Round;
|
StrokeArrow.LineJoin = PenLineJoin.Round;
|
||||||
|
|
||||||
RenderTransform = _scale;
|
RenderTransform = _scale;
|
||||||
|
|
||||||
Width = Height = 22;
|
Width = Height = size;
|
||||||
FontSize = Width / 1.55;
|
FontSize = Width / 1.55;
|
||||||
|
|
||||||
Background = background;
|
Background = background;
|
||||||
|
|||||||
@@ -137,13 +137,13 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Stretch"/>
|
VerticalAlignment="Stretch"/>
|
||||||
|
|
||||||
<controls:CanvasEditor
|
<Border Grid.Column="2" ClipToBounds="True">
|
||||||
Grid.Column="2"
|
<controls:CanvasEditor
|
||||||
x:Name="GpsImageEditor"
|
x:Name="GpsImageEditor"
|
||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
HorizontalAlignment="Stretch" >
|
HorizontalAlignment="Stretch" />
|
||||||
</controls:CanvasEditor>
|
</Border>
|
||||||
|
|
||||||
<GridSplitter
|
<GridSplitter
|
||||||
Background="DarkGray"
|
Background="DarkGray"
|
||||||
ResizeDirection="Columns"
|
ResizeDirection="Columns"
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Azaion.Common;
|
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
@@ -14,7 +11,6 @@ using Azaion.Common.Extensions;
|
|||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using GMap.NET;
|
using GMap.NET;
|
||||||
using GMap.NET.MapProviders;
|
using GMap.NET.MapProviders;
|
||||||
using GMap.NET.WindowsPresentation;
|
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
|
|
||||||
namespace Azaion.Annotator.Controls;
|
namespace Azaion.Annotator.Controls;
|
||||||
@@ -23,7 +19,7 @@ public partial class MapMatcher : UserControl
|
|||||||
{
|
{
|
||||||
private AppConfig _appConfig = null!;
|
private AppConfig _appConfig = null!;
|
||||||
List<MediaFileInfo> _allMediaFiles = new();
|
List<MediaFileInfo> _allMediaFiles = new();
|
||||||
private Dictionary<int, Annotation> _annotations = new();
|
public Dictionary<int, Annotation> Annotations = new();
|
||||||
private string _currentDir = null!;
|
private string _currentDir = null!;
|
||||||
private IGpsMatcherService _gpsMatcherService = null!;
|
private IGpsMatcherService _gpsMatcherService = null!;
|
||||||
|
|
||||||
@@ -47,7 +43,7 @@ public partial class MapMatcher : UserControl
|
|||||||
private async Task OpenGpsLocation(int gpsFilesIndex)
|
private async Task OpenGpsLocation(int gpsFilesIndex)
|
||||||
{
|
{
|
||||||
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
|
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
|
||||||
var ann = _annotations.GetValueOrDefault(gpsFilesIndex);
|
var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
|
||||||
if (ann == null)
|
if (ann == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -101,51 +97,14 @@ public partial class MapMatcher : UserControl
|
|||||||
_allMediaFiles = mediaFiles;
|
_allMediaFiles = mediaFiles;
|
||||||
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
|
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
|
||||||
|
|
||||||
_annotations = mediaFiles.Select((x, i) => (i, new Annotation
|
Annotations = mediaFiles.Select((x, i) => (i, new Annotation
|
||||||
{
|
{
|
||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
OriginalMediaName = x.Name
|
OriginalMediaName = x.Name
|
||||||
})).ToDictionary(x => x.i, x => x.Item2);
|
})).ToDictionary(x => x.i, x => x.Item2);
|
||||||
|
|
||||||
var initialLat = double.Parse(TbLat.Text);
|
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||||
var initialLon = double.Parse(TbLon.Text);
|
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon);
|
||||||
|
|
||||||
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SetMarker(GpsMatchResult result)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude));
|
|
||||||
var ann = _annotations[result.Index];
|
|
||||||
marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue)
|
|
||||||
{
|
|
||||||
Text = ann.Name
|
|
||||||
};
|
|
||||||
SatelliteMap.Markers.Add(marker);
|
|
||||||
ann.Lat = result.Latitude;
|
|
||||||
ann.Lon = result.Longitude;
|
|
||||||
SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude);
|
|
||||||
SatelliteMap.ZoomAndCenterMarkers(null);
|
|
||||||
});
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetFromCsv(List<MediaFileInfo> mediaFiles)
|
|
||||||
{
|
|
||||||
|
|
||||||
var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH);
|
|
||||||
var csvDict = csvResults
|
|
||||||
.Where(x => x.MatchType == "stitched")
|
|
||||||
.ToDictionary(x => x.Index);
|
|
||||||
foreach (var ann in _annotations)
|
|
||||||
{
|
|
||||||
var csvRes = csvDict.GetValueOrDefault(ann.Key);
|
|
||||||
if (csvRes == null)
|
|
||||||
continue;
|
|
||||||
await SetMarker(csvRes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void TestGps(object sender, RoutedEventArgs e)
|
private async void TestGps(object sender, RoutedEventArgs e)
|
||||||
@@ -153,8 +112,7 @@ public partial class MapMatcher : UserControl
|
|||||||
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
|
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var initialLat = double.Parse(TbLat.Text);
|
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||||
var initialLon = double.Parse(TbLon.Text);
|
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon);
|
||||||
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
using System.Drawing;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator.Extensions;
|
|
||||||
|
|
||||||
public static class RectangleFExtensions
|
|
||||||
{
|
|
||||||
public static double Area(this RectangleF rectangle) => rectangle.Width * rectangle.Height;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator;
|
|
||||||
|
|
||||||
public static class SynchronizeInvokeExtensions
|
|
||||||
{
|
|
||||||
public static void InvokeEx<T>(this T t, Action<T> action) where T : ISynchronizeInvoke
|
|
||||||
{
|
|
||||||
if (t.InvokeRequired)
|
|
||||||
t.Invoke(action, [t]);
|
|
||||||
else
|
|
||||||
action(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,28 +4,29 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
|
<LangVersion>12</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
|
<PackageReference Include="LazyCache" Version="2.4.0" />
|
||||||
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
|
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
|
||||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="MessagePack" Version="3.1.0" />
|
<PackageReference Include="MessagePack" Version="3.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
|
||||||
|
<PackageReference Include="NetMQ" Version="4.0.1.16" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Polly" Version="8.5.2" />
|
<PackageReference Include="Polly" Version="8.5.2" />
|
||||||
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
|
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+203
-56
@@ -1,111 +1,186 @@
|
|||||||
using System.Windows;
|
using System.Diagnostics;
|
||||||
using System.Windows.Media;
|
using System.IO;
|
||||||
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 Newtonsoft.Json;
|
||||||
|
using Serilog;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
namespace Azaion.Common;
|
namespace Azaion.Common;
|
||||||
|
|
||||||
public class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
public const string JPG_EXT = ".jpg";
|
public const string CONFIG_PATH = "config.json";
|
||||||
|
public const string DEFAULT_API_URL = "https://api.azaion.com";
|
||||||
|
public const string AZAION_SUITE_EXE = "Azaion.Suite.exe";
|
||||||
|
|
||||||
|
public const int AI_TILE_SIZE_DEFAULT = 1280;
|
||||||
|
|
||||||
|
#region ExternalClientsConfig
|
||||||
|
|
||||||
|
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
|
||||||
|
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
|
||||||
|
private static readonly LoaderClientConfig DefaultLoaderClientConfig = new()
|
||||||
|
{
|
||||||
|
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
|
||||||
|
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
|
||||||
|
ApiUrl = DEFAULT_API_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
|
||||||
|
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
|
||||||
|
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
|
||||||
|
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
|
||||||
|
|
||||||
|
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
|
||||||
|
private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
|
||||||
|
|
||||||
|
private static readonly InferenceClientConfig DefaultInferenceClientConfig = new()
|
||||||
|
{
|
||||||
|
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
|
||||||
|
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
|
||||||
|
ApiUrl = DEFAULT_API_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
|
||||||
|
private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255;
|
||||||
|
private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256;
|
||||||
|
private static readonly GpsDeniedClientConfig DefaultGpsDeniedClientConfig = new()
|
||||||
|
{
|
||||||
|
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
|
||||||
|
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
|
||||||
|
ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion ExternalClientsConfig
|
||||||
|
|
||||||
|
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
|
||||||
|
|
||||||
|
public const string JPG_EXT = ".jpg";
|
||||||
|
public const string TXT_EXT = ".txt";
|
||||||
#region DirectoriesConfig
|
#region DirectoriesConfig
|
||||||
|
|
||||||
public const string DEFAULT_VIDEO_DIR = "video";
|
public const string DEFAULT_VIDEO_DIR = "video";
|
||||||
public const string DEFAULT_LABELS_DIR = "labels";
|
private const string DEFAULT_LABELS_DIR = "labels";
|
||||||
public const string DEFAULT_IMAGES_DIR = "images";
|
private const string DEFAULT_IMAGES_DIR = "images";
|
||||||
public const string DEFAULT_RESULTS_DIR = "results";
|
private const string DEFAULT_RESULTS_DIR = "results";
|
||||||
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
||||||
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
||||||
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region AnnotatorConfig
|
#region AnnotatorConfig
|
||||||
|
|
||||||
public static readonly AnnotationConfig DefaultAnnotationConfig = new()
|
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
||||||
|
[
|
||||||
|
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 },
|
||||||
|
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 },
|
||||||
|
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 },
|
||||||
|
new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 },
|
||||||
|
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 },
|
||||||
|
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 },
|
||||||
|
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 },
|
||||||
|
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 },
|
||||||
|
new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 },
|
||||||
|
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 },
|
||||||
|
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 },
|
||||||
|
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor(), MaxSizeM = 3 },
|
||||||
|
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor(), MaxSizeM = 14 },
|
||||||
|
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor(), MaxSizeM = 8 },
|
||||||
|
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor(), MaxSizeM = 15 },
|
||||||
|
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor(), MaxSizeM = 20 },
|
||||||
|
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor(), MaxSizeM = 10 },
|
||||||
|
new() { Id = 17, Name = "Ammo", ShortName = "БК", Color = "#33658a".ToColor(), MaxSizeM = 2 },
|
||||||
|
new() { Id = 18, Name = "Protect.Struct", ShortName = "Зуби.драк", Color = "#969647".ToColor(), MaxSizeM = 2 }
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi", "ts"];
|
||||||
|
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
||||||
|
|
||||||
|
private static readonly AnnotationConfig DefaultAnnotationConfig = new()
|
||||||
{
|
{
|
||||||
DetectionClasses = DefaultAnnotationClasses!,
|
DetectionClasses = DefaultAnnotationClasses,
|
||||||
VideoFormats = DefaultVideoFormats!,
|
VideoFormats = DefaultVideoFormats,
|
||||||
ImageFormats = DefaultImageFormats!,
|
ImageFormats = DefaultImageFormats,
|
||||||
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
|
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly List<DetectionClass> DefaultAnnotationClasses =
|
#region UIConfig
|
||||||
[
|
public const int DEFAULT_LEFT_PANEL_WIDTH = 200;
|
||||||
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
|
public const int DEFAULT_RIGHT_PANEL_WIDTH = 200;
|
||||||
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
|
#endregion UIConfig
|
||||||
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
|
|
||||||
new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
|
|
||||||
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
|
|
||||||
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
|
|
||||||
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
|
|
||||||
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
|
|
||||||
new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() },
|
|
||||||
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
|
|
||||||
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
|
|
||||||
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
|
|
||||||
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
|
|
||||||
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
|
|
||||||
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
|
|
||||||
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }
|
|
||||||
];
|
|
||||||
|
|
||||||
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
#region CameraConfig
|
||||||
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
|
||||||
|
|
||||||
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
public const int DEFAULT_ALTITUDE = 400;
|
||||||
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m;
|
||||||
|
public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m;
|
||||||
|
|
||||||
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
public static readonly CameraConfig DefaultCameraConfig = new()
|
||||||
|
{
|
||||||
|
Altitude = DEFAULT_ALTITUDE,
|
||||||
|
CameraFocalLength = DEFAULT_CAMERA_FOCAL_LENGTH,
|
||||||
|
CameraSensorWidth = DEFAULT_CAMERA_SENSOR_WIDTH
|
||||||
|
};
|
||||||
|
|
||||||
|
#endregion CameraConfig
|
||||||
|
|
||||||
|
private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
||||||
|
|
||||||
# endregion AnnotatorConfig
|
# endregion AnnotatorConfig
|
||||||
|
|
||||||
# region AIRecognitionConfig
|
# region AIRecognitionConfig
|
||||||
|
|
||||||
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
|
private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
|
||||||
{
|
{
|
||||||
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
|
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
|
||||||
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
|
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
|
||||||
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
|
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
|
||||||
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
|
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
|
||||||
|
BigImageTileOverlapPercent = DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT,
|
||||||
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
|
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
|
||||||
};
|
};
|
||||||
|
|
||||||
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
||||||
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
||||||
public const double TRACKING_PROBABILITY_INCREASE = 15;
|
private const double TRACKING_PROBABILITY_INCREASE = 15;
|
||||||
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
||||||
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
|
||||||
|
private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
||||||
|
|
||||||
public const int DETECTION_BATCH_SIZE = 4;
|
|
||||||
# endregion AIRecognitionConfig
|
# endregion AIRecognitionConfig
|
||||||
|
|
||||||
|
# region GpsDeniedConfig
|
||||||
|
|
||||||
|
private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
|
||||||
|
{
|
||||||
|
MinKeyPoints = 11
|
||||||
|
};
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
#region Thumbnails
|
#region Thumbnails
|
||||||
|
|
||||||
public static readonly ThumbnailConfig DefaultThumbnailConfig = new()
|
private static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||||
|
|
||||||
|
private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
|
||||||
{
|
{
|
||||||
Size = DefaultThumbnailSize,
|
Size = DefaultThumbnailSize,
|
||||||
Border = DEFAULT_THUMBNAIL_BORDER
|
Border = DEFAULT_THUMBNAIL_BORDER
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
private const int DEFAULT_THUMBNAIL_BORDER = 10;
|
||||||
|
|
||||||
public const int DEFAULT_THUMBNAIL_BORDER = 10;
|
|
||||||
|
|
||||||
public const string THUMBNAIL_PREFIX = "_thumb";
|
public const string THUMBNAIL_PREFIX = "_thumb";
|
||||||
public const string RESULT_PREFIX = "_result";
|
public const string RESULT_PREFIX = "_result";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Queue
|
|
||||||
|
|
||||||
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
||||||
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Database
|
#region Database
|
||||||
|
|
||||||
@@ -113,6 +188,7 @@ public class Constants
|
|||||||
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
|
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
|
||||||
public const string ADMIN_EMAIL = "admin@azaion.com";
|
public const string ADMIN_EMAIL = "admin@azaion.com";
|
||||||
public const string DETECTIONS_TABLENAME = "detections";
|
public const string DETECTIONS_TABLENAME = "detections";
|
||||||
|
public const string MEDIAFILE_TABLENAME = "mediafiles";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -124,5 +200,76 @@ public class Constants
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public const string CSV_PATH = "matches.csv";
|
public const string SPLIT_SUFFIX = "!split!";
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly InitConfig DefaultInitConfig = new()
|
||||||
|
{
|
||||||
|
LoaderClientConfig = DefaultLoaderClientConfig,
|
||||||
|
InferenceClientConfig = DefaultInferenceClientConfig,
|
||||||
|
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
|
||||||
|
DirectoriesConfig = new DirectoriesConfig
|
||||||
|
{
|
||||||
|
ApiResourcesDirectory = ""
|
||||||
|
},
|
||||||
|
CameraConfig = DefaultCameraConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly AppConfig FailsafeAppConfig = new()
|
||||||
|
{
|
||||||
|
AnnotationConfig = DefaultAnnotationConfig,
|
||||||
|
|
||||||
|
UIConfig = new UIConfig
|
||||||
|
{
|
||||||
|
LeftPanelWidth = DEFAULT_LEFT_PANEL_WIDTH,
|
||||||
|
RightPanelWidth = DEFAULT_RIGHT_PANEL_WIDTH,
|
||||||
|
GenerateAnnotatedImage = false
|
||||||
|
},
|
||||||
|
|
||||||
|
DirectoriesConfig = new DirectoriesConfig
|
||||||
|
{
|
||||||
|
VideosDirectory = DEFAULT_VIDEO_DIR,
|
||||||
|
ImagesDirectory = DEFAULT_IMAGES_DIR,
|
||||||
|
LabelsDirectory = DEFAULT_LABELS_DIR,
|
||||||
|
ResultsDirectory = DEFAULT_RESULTS_DIR,
|
||||||
|
ThumbnailsDirectory = DEFAULT_THUMBNAILS_DIR,
|
||||||
|
GpsSatDirectory = DEFAULT_GPS_SAT_DIRECTORY,
|
||||||
|
GpsRouteDirectory = DEFAULT_GPS_ROUTE_DIRECTORY
|
||||||
|
},
|
||||||
|
|
||||||
|
ThumbnailConfig = DefaultThumbnailConfig,
|
||||||
|
AIRecognitionConfig = DefaultAIRecognitionConfig,
|
||||||
|
GpsDeniedConfig = DefaultGpsDeniedConfig,
|
||||||
|
|
||||||
|
LoaderClientConfig = DefaultLoaderClientConfig,
|
||||||
|
InferenceClientConfig = DefaultInferenceClientConfig,
|
||||||
|
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
|
||||||
|
CameraConfig = DefaultCameraConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
public static InitConfig ReadInitConfig(ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(CONFIG_PATH))
|
||||||
|
throw new FileNotFoundException(CONFIG_PATH);
|
||||||
|
var configStr = File.ReadAllText(CONFIG_PATH);
|
||||||
|
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
|
||||||
|
|
||||||
|
return config ?? DefaultInitConfig;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.Error(e, e.Message);
|
||||||
|
return DefaultInitConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Version GetLocalVersion()
|
||||||
|
{
|
||||||
|
var localFileInfo = FileVersionInfo.GetVersionInfo(AZAION_SUITE_EXE);
|
||||||
|
if (string.IsNullOrWhiteSpace(localFileInfo.ProductVersion))
|
||||||
|
throw new Exception($"Can't find {AZAION_SUITE_EXE} and its version");
|
||||||
|
return new Version(localFileInfo.FileVersion!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<UserControl x:Class="Azaion.Common.Controls.CameraConfigControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:cfg="clr-namespace:Azaion.Common.DTO.Config"
|
||||||
|
xmlns:controls="clr-namespace:Azaion.Common.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="120" d:DesignWidth="360">
|
||||||
|
<Grid Margin="4" Background="Black">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="65"/>
|
||||||
|
<ColumnDefinition Width="70"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="70"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Altitude -->
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||||
|
Foreground="LightGray"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0" Text="Altitude, m:"/>
|
||||||
|
<Slider x:Name="AltitudeSlider" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
|
||||||
|
Minimum="0" Maximum="10000" TickFrequency="100"
|
||||||
|
IsSnapToTickEnabled="False"
|
||||||
|
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
<controls:NumericUpDown x:Name="AltitudeNud"
|
||||||
|
Grid.Row="0" Grid.Column="3"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MinValue="50"
|
||||||
|
MaxValue="5000"
|
||||||
|
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl},
|
||||||
|
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Step="10">
|
||||||
|
|
||||||
|
</controls:NumericUpDown>
|
||||||
|
|
||||||
|
<!-- Focal length -->
|
||||||
|
<TextBlock
|
||||||
|
Foreground="LightGray"
|
||||||
|
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,8,8,0" Text="Focal length, mm:"/>
|
||||||
|
<controls:NumericUpDown x:Name="FocalNud"
|
||||||
|
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
|
||||||
|
MinValue="0.1"
|
||||||
|
MaxValue="100"
|
||||||
|
Step="0.05"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Value="{Binding Camera.CameraFocalLength, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
</controls:NumericUpDown>
|
||||||
|
|
||||||
|
<!-- Sensor width -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="LightGray"
|
||||||
|
Margin="0,8,8,0" Text="Sensor width, mm:"/>
|
||||||
|
<controls:NumericUpDown x:Name="SensorNud"
|
||||||
|
Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Step="0.05"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
MinValue="0.1"
|
||||||
|
MaxValue="100"
|
||||||
|
Value="{Binding Camera.CameraSensorWidth, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
</controls:NumericUpDown>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Controls;
|
||||||
|
|
||||||
|
public partial class CameraConfigControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty CameraProperty = DependencyProperty.Register(
|
||||||
|
nameof(Camera), typeof(CameraConfig), typeof(CameraConfigControl),
|
||||||
|
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
|
||||||
|
|
||||||
|
public CameraConfig Camera
|
||||||
|
{
|
||||||
|
get => (CameraConfig)GetValue(CameraProperty) ?? new CameraConfig();
|
||||||
|
set => SetValue(CameraProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fires whenever any camera parameter value changes in UI
|
||||||
|
public event EventHandler? CameraChanged;
|
||||||
|
|
||||||
|
public CameraConfigControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = this;
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Hook up change notifications
|
||||||
|
if (AltitudeSlider != null)
|
||||||
|
AltitudeSlider.ValueChanged += (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
SubscribeNud(AltitudeNud);
|
||||||
|
SubscribeNud(FocalNud);
|
||||||
|
SubscribeNud(SensorNud);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeNud(UserControl? nud)
|
||||||
|
{
|
||||||
|
if (nud is NumericUpDown num)
|
||||||
|
{
|
||||||
|
var dpd = DependencyPropertyDescriptor.FromProperty(NumericUpDown.ValueProperty, typeof(NumericUpDown));
|
||||||
|
dpd?.AddValueChanged(num, (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes the control with the provided CameraConfig instance and wires two-way binding via dependency property
|
||||||
|
public void Init(CameraConfig cameraConfig)
|
||||||
|
{
|
||||||
|
Camera = cameraConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
using System.Windows;
|
using System.Drawing;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using Azaion.Annotator.DTO;
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Color = System.Windows.Media.Color;
|
using Color = System.Windows.Media.Color;
|
||||||
|
using Image = System.Windows.Controls.Image;
|
||||||
|
using Point = System.Windows.Point;
|
||||||
using Rectangle = System.Windows.Shapes.Rectangle;
|
using Rectangle = System.Windows.Shapes.Rectangle;
|
||||||
|
using Size = System.Windows.Size;
|
||||||
|
|
||||||
namespace Azaion.Common.Controls;
|
namespace Azaion.Common.Controls;
|
||||||
|
|
||||||
@@ -25,12 +31,17 @@ public class CanvasEditor : Canvas
|
|||||||
|
|
||||||
private Rectangle _curRec = new();
|
private Rectangle _curRec = new();
|
||||||
private DetectionControl _curAnn = null!;
|
private DetectionControl _curAnn = null!;
|
||||||
|
|
||||||
private const int MIN_SIZE = 20;
|
|
||||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
|
||||||
|
|
||||||
public IMediator Mediator { get; set; } = null!;
|
|
||||||
|
|
||||||
|
private readonly MatrixTransform _matrixTransform = new();
|
||||||
|
private Point _panStartPoint;
|
||||||
|
private bool _isZoomedIn;
|
||||||
|
|
||||||
|
private const int MIN_SIZE = 12;
|
||||||
|
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||||
|
|
||||||
|
public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform };
|
||||||
|
private RectangleF? _clampedRect;
|
||||||
|
|
||||||
public static readonly DependencyProperty GetTimeFuncProp =
|
public static readonly DependencyProperty GetTimeFuncProp =
|
||||||
DependencyProperty.Register(
|
DependencyProperty.Register(
|
||||||
nameof(GetTimeFunc),
|
nameof(GetTimeFunc),
|
||||||
@@ -99,22 +110,61 @@ public class CanvasEditor : Canvas
|
|||||||
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
|
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
|
||||||
};
|
};
|
||||||
|
|
||||||
KeyDown += (_, args) =>
|
|
||||||
{
|
|
||||||
Console.WriteLine($"pressed {args.Key}");
|
|
||||||
};
|
|
||||||
MouseDown += CanvasMouseDown;
|
MouseDown += CanvasMouseDown;
|
||||||
MouseMove += CanvasMouseMove;
|
MouseMove += CanvasMouseMove;
|
||||||
MouseUp += CanvasMouseUp;
|
MouseUp += CanvasMouseUp;
|
||||||
SizeChanged += CanvasResized;
|
SizeChanged += CanvasResized;
|
||||||
Cursor = Cursors.Cross;
|
Cursor = Cursors.Cross;
|
||||||
|
Children.Insert(0, BackgroundImage);
|
||||||
Children.Add(_newAnnotationRect);
|
Children.Add(_newAnnotationRect);
|
||||||
Children.Add(_horizontalLine);
|
Children.Add(_horizontalLine);
|
||||||
Children.Add(_verticalLine);
|
Children.Add(_verticalLine);
|
||||||
Children.Add(_classNameHint);
|
Children.Add(_classNameHint);
|
||||||
|
|
||||||
Loaded += Init;
|
Loaded += Init;
|
||||||
|
RenderTransform = _matrixTransform;
|
||||||
|
MouseWheel += CanvasWheel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetBackground(ImageSource? source)
|
||||||
|
{
|
||||||
|
SetZoom();
|
||||||
|
BackgroundImage.Source = source;
|
||||||
|
UpdateClampedRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetZoom(Matrix? matrix = null)
|
||||||
|
{
|
||||||
|
if (matrix == null)
|
||||||
|
{
|
||||||
|
_matrixTransform.Matrix = Matrix.Identity;
|
||||||
|
_isZoomedIn = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_matrixTransform.Matrix = matrix.Value;
|
||||||
|
_isZoomedIn = true;
|
||||||
|
}
|
||||||
|
// foreach (var detection in CurrentDetections)
|
||||||
|
// detection.UpdateAdornerScale(scale: _matrixTransform.Matrix.M11);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CanvasWheel(object sender, MouseWheelEventArgs e)
|
||||||
|
{
|
||||||
|
if (Keyboard.Modifiers != ModifierKeys.Control)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var mousePos = e.GetPosition(this);
|
||||||
|
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
|
||||||
|
|
||||||
|
var matrix = _matrixTransform.Matrix;
|
||||||
|
if (scale < 1 && matrix.M11 * scale < 1.0)
|
||||||
|
SetZoom();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
matrix.ScaleAt(scale, scale, mousePos.X, mousePos.Y);
|
||||||
|
SetZoom(matrix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Init(object sender, RoutedEventArgs e)
|
private void Init(object sender, RoutedEventArgs e)
|
||||||
@@ -128,62 +178,167 @@ public class CanvasEditor : Canvas
|
|||||||
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
ClearSelections();
|
ClearSelections();
|
||||||
NewAnnotationStart(sender, e);
|
if (e.LeftButton != MouseButtonState.Pressed)
|
||||||
|
return;
|
||||||
|
if (Keyboard.Modifiers == ModifierKeys.Control && _isZoomedIn)
|
||||||
|
{
|
||||||
|
_panStartPoint = e.GetPosition(this);
|
||||||
|
SelectionState = SelectionState.PanZoomMoving;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
NewAnnotationStart(sender, e);
|
||||||
|
(sender as UIElement)?.CaptureMouse();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CanvasMouseMove(object sender, MouseEventArgs e)
|
private void CanvasMouseMove(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
var pos = e.GetPosition(this);
|
var pos = GetClampedPosition(e);
|
||||||
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
|
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
|
||||||
_verticalLine.X1 = _verticalLine.X2 = pos.X;
|
_verticalLine.X1 = _verticalLine.X2 = pos.X;
|
||||||
SetLeft(_classNameHint, pos.X + 10);
|
SetLeft(_classNameHint, pos.X + 10);
|
||||||
SetTop(_classNameHint, pos.Y - 30);
|
SetTop(_classNameHint, pos.Y - 30);
|
||||||
|
|
||||||
if (e.LeftButton != MouseButtonState.Pressed)
|
|
||||||
return;
|
|
||||||
if (SelectionState == SelectionState.NewAnnCreating)
|
|
||||||
NewAnnotationCreatingMove(sender, e);
|
|
||||||
|
|
||||||
if (SelectionState == SelectionState.AnnResizing)
|
|
||||||
AnnotationResizeMove(sender, e);
|
|
||||||
|
|
||||||
if (SelectionState == SelectionState.AnnMoving)
|
switch (SelectionState)
|
||||||
AnnotationPositionMove(sender, e);
|
{
|
||||||
|
case SelectionState.NewAnnCreating:
|
||||||
|
NewAnnotationCreatingMove(pos);
|
||||||
|
break;
|
||||||
|
case SelectionState.AnnResizing:
|
||||||
|
AnnotationResizeMove(pos);
|
||||||
|
break;
|
||||||
|
case SelectionState.AnnMoving:
|
||||||
|
AnnotationPositionMove(pos);
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
case SelectionState.PanZoomMoving:
|
||||||
|
PanZoomMove(pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Point GetClampedPosition(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
var pos = e.GetPosition(this);
|
||||||
|
return !_clampedRect.HasValue
|
||||||
|
? pos
|
||||||
|
: new Point
|
||||||
|
(
|
||||||
|
Math.Clamp(pos.X, _clampedRect.Value.X, _clampedRect.Value.Right),
|
||||||
|
Math.Clamp(pos.Y, _clampedRect.Value.Y, _clampedRect.Value.Bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PanZoomMove(Point point)
|
||||||
|
{
|
||||||
|
var delta = point - _panStartPoint;
|
||||||
|
|
||||||
|
var matrix = _matrixTransform.Matrix;
|
||||||
|
matrix.Translate(delta.X, delta.Y);
|
||||||
|
|
||||||
|
_matrixTransform.Matrix = matrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
|
(sender as UIElement)?.ReleaseMouseCapture();
|
||||||
if (SelectionState == SelectionState.NewAnnCreating)
|
if (SelectionState == SelectionState.NewAnnCreating)
|
||||||
{
|
{
|
||||||
var endPos = e.GetPosition(this);
|
var endPos = GetClampedPosition(e);
|
||||||
_newAnnotationRect.Width = 0;
|
_newAnnotationRect.Width = 0;
|
||||||
_newAnnotationRect.Height = 0;
|
_newAnnotationRect.Height = 0;
|
||||||
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
||||||
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
||||||
if (width < MIN_SIZE || height < MIN_SIZE)
|
if (width >= MIN_SIZE && height >= MIN_SIZE)
|
||||||
return;
|
|
||||||
|
|
||||||
var time = GetTimeFunc();
|
|
||||||
CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
|
|
||||||
{
|
{
|
||||||
Width = width,
|
var time = GetTimeFunc();
|
||||||
Height = height,
|
var control = CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
|
||||||
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
{
|
||||||
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
|
Width = width,
|
||||||
Confidence = 1
|
Height = height,
|
||||||
});
|
Left = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
||||||
|
Top = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
|
||||||
|
Confidence = 1
|
||||||
|
});
|
||||||
|
control.UpdateLayout();
|
||||||
|
CheckLabelBoundaries(control);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else if (SelectionState != SelectionState.PanZoomMoving)
|
||||||
|
CheckLabelBoundaries(_curAnn);
|
||||||
|
|
||||||
SelectionState = SelectionState.None;
|
SelectionState = SelectionState.None;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckLabelBoundaries(DetectionControl detectionControl)
|
||||||
|
{
|
||||||
|
var lb = detectionControl.DetectionLabelContainer;
|
||||||
|
var origin = lb.TranslatePoint(new Point(0, 0), this);
|
||||||
|
lb.Children[0].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||||
|
var size = lb.Children[0].DesiredSize;
|
||||||
|
var controlLabel = new RectangleF((float)origin.X, (float)origin.Y, (float)size.Width, (float)size.Height);
|
||||||
|
|
||||||
|
foreach (var c in CurrentDetections)
|
||||||
|
{
|
||||||
|
if (c == detectionControl)
|
||||||
|
continue;
|
||||||
|
var detRect = new RectangleF((float)GetLeft(c), (float)GetTop(c), (float)c.Width, (float)c.Height);
|
||||||
|
detRect.Intersect(controlLabel);
|
||||||
|
|
||||||
|
|
||||||
|
// var intersect = detections[i].ToRectangle();
|
||||||
|
// intersect.Intersect(detections[j].ToRectangle());
|
||||||
|
|
||||||
|
// detectionControl.
|
||||||
|
// var otherControls = allControls.Where(c => c != control);
|
||||||
|
// control.UpdateLabelPosition(otherControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CanvasResized(object sender, SizeChangedEventArgs e)
|
private void CanvasResized(object sender, SizeChangedEventArgs e)
|
||||||
{
|
{
|
||||||
_horizontalLine.X2 = e.NewSize.Width;
|
_horizontalLine.X2 = e.NewSize.Width;
|
||||||
_verticalLine.Y2 = e.NewSize.Height;
|
_verticalLine.Y2 = e.NewSize.Height;
|
||||||
|
BackgroundImage.Width = e.NewSize.Width;
|
||||||
|
BackgroundImage.Height = e.NewSize.Height;
|
||||||
|
UpdateClampedRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateClampedRect()
|
||||||
|
{
|
||||||
|
if (BackgroundImage.Source is not BitmapSource imageSource)
|
||||||
|
{
|
||||||
|
_clampedRect = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgWidth = imageSource.PixelWidth;
|
||||||
|
var imgHeight = imageSource.PixelHeight;
|
||||||
|
var canvasWidth = ActualWidth;
|
||||||
|
var canvasHeight = ActualHeight;
|
||||||
|
|
||||||
|
var imgRatio = imgWidth / (double)imgHeight;
|
||||||
|
var canvasRatio = canvasWidth / canvasHeight;
|
||||||
|
|
||||||
|
double renderedWidth;
|
||||||
|
double renderedHeight;
|
||||||
|
|
||||||
|
if (imgRatio > canvasRatio)
|
||||||
|
{
|
||||||
|
renderedWidth = canvasWidth;
|
||||||
|
renderedHeight = canvasWidth / imgRatio;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
renderedHeight = canvasHeight;
|
||||||
|
renderedWidth = canvasHeight * imgRatio;
|
||||||
|
}
|
||||||
|
var xOffset = (canvasWidth - renderedWidth) / 2;
|
||||||
|
var yOffset = (canvasHeight - renderedHeight) / 2;
|
||||||
|
|
||||||
|
_clampedRect = new RectangleF((float)xOffset, (float)yOffset, (float)renderedWidth, (float)renderedHeight);
|
||||||
|
}
|
||||||
|
|
||||||
#region Annotation Resizing & Moving
|
#region Annotation Resizing & Moving
|
||||||
|
|
||||||
private void AnnotationResizeStart(object sender, MouseEventArgs e)
|
private void AnnotationResizeStart(object sender, MouseEventArgs e)
|
||||||
@@ -192,20 +347,19 @@ public class CanvasEditor : Canvas
|
|||||||
_lastPos = e.GetPosition(this);
|
_lastPos = e.GetPosition(this);
|
||||||
_curRec = (Rectangle)sender;
|
_curRec = (Rectangle)sender;
|
||||||
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
|
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
|
||||||
|
(sender as UIElement)?.CaptureMouse();
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AnnotationResizeMove(object sender, MouseEventArgs e)
|
private void AnnotationResizeMove(Point point)
|
||||||
{
|
{
|
||||||
if (SelectionState != SelectionState.AnnResizing)
|
if (SelectionState != SelectionState.AnnResizing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var currentPos = e.GetPosition(this);
|
|
||||||
|
|
||||||
var x = GetLeft(_curAnn);
|
var x = GetLeft(_curAnn);
|
||||||
var y = GetTop(_curAnn);
|
var y = GetTop(_curAnn);
|
||||||
var offsetX = currentPos.X - _lastPos.X;
|
var offsetX = point.X - _lastPos.X;
|
||||||
var offsetY = currentPos.Y - _lastPos.Y;
|
var offsetY = point.Y - _lastPos.Y;
|
||||||
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
|
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
|
||||||
{
|
{
|
||||||
case (HorizontalAlignment.Left, VerticalAlignment.Top):
|
case (HorizontalAlignment.Left, VerticalAlignment.Top):
|
||||||
@@ -245,7 +399,7 @@ public class CanvasEditor : Canvas
|
|||||||
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_lastPos = currentPos;
|
_lastPos = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AnnotationPositionStart(object sender, MouseEventArgs e)
|
private void AnnotationPositionStart(object sender, MouseEventArgs e)
|
||||||
@@ -257,24 +411,31 @@ public class CanvasEditor : Canvas
|
|||||||
ClearSelections();
|
ClearSelections();
|
||||||
|
|
||||||
_curAnn.IsSelected = true;
|
_curAnn.IsSelected = true;
|
||||||
|
|
||||||
SelectionState = SelectionState.AnnMoving;
|
SelectionState = SelectionState.AnnMoving;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AnnotationPositionMove(object sender, MouseEventArgs e)
|
private void AnnotationPositionMove(Point point)
|
||||||
{
|
{
|
||||||
if (SelectionState != SelectionState.AnnMoving)
|
if (SelectionState != SelectionState.AnnMoving)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var currentPos = e.GetPosition(this);
|
var offsetX = point.X - _lastPos.X;
|
||||||
var offsetX = currentPos.X - _lastPos.X;
|
var offsetY = point.Y - _lastPos.Y;
|
||||||
var offsetY = currentPos.Y - _lastPos.Y;
|
|
||||||
|
var nextLeft = GetLeft(_curAnn) + offsetX;
|
||||||
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX);
|
var nextTop = GetTop(_curAnn) + offsetY;
|
||||||
SetTop(_curAnn, GetTop(_curAnn) + offsetY);
|
|
||||||
_lastPos = currentPos;
|
if (_clampedRect.HasValue)
|
||||||
e.Handled = true;
|
{
|
||||||
|
nextLeft = Math.Clamp(nextLeft, _clampedRect.Value.X, _clampedRect.Value.Right - _curAnn.Width);
|
||||||
|
nextTop = Math.Clamp(nextTop, _clampedRect.Value.Y, _clampedRect.Value.Bottom - _curAnn.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetLeft(_curAnn, nextLeft);
|
||||||
|
SetTop(_curAnn, nextTop);
|
||||||
|
_lastPos = point;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -284,50 +445,67 @@ public class CanvasEditor : Canvas
|
|||||||
private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
|
private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
_newAnnotationStartPos = e.GetPosition(this);
|
_newAnnotationStartPos = e.GetPosition(this);
|
||||||
|
|
||||||
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
|
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
|
||||||
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
|
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
|
||||||
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
|
_newAnnotationRect.MouseMove += (sender, e) =>
|
||||||
|
{
|
||||||
|
var currentPos = e.GetPosition(this);
|
||||||
|
NewAnnotationCreatingMove(currentPos);
|
||||||
|
};
|
||||||
|
|
||||||
SelectionState = SelectionState.NewAnnCreating;
|
SelectionState = SelectionState.NewAnnCreating;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
|
private void NewAnnotationCreatingMove(Point point)
|
||||||
{
|
{
|
||||||
if (SelectionState != SelectionState.NewAnnCreating)
|
if (SelectionState != SelectionState.NewAnnCreating)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var currentPos = e.GetPosition(this);
|
var diff = point - _newAnnotationStartPos;
|
||||||
var diff = currentPos - _newAnnotationStartPos;
|
|
||||||
|
|
||||||
_newAnnotationRect.Height = Math.Abs(diff.Y);
|
_newAnnotationRect.Height = Math.Abs(diff.Y);
|
||||||
_newAnnotationRect.Width = Math.Abs(diff.X);
|
_newAnnotationRect.Width = Math.Abs(diff.X);
|
||||||
|
|
||||||
if (diff.X < 0)
|
if (diff.X < 0)
|
||||||
SetLeft(_newAnnotationRect, currentPos.X);
|
SetLeft(_newAnnotationRect, point.X);
|
||||||
if (diff.Y < 0)
|
if (diff.Y < 0)
|
||||||
SetTop(_newAnnotationRect, currentPos.Y);
|
SetTop(_newAnnotationRect, point.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateDetections(TimeSpan time, IEnumerable<Detection> detections, List<DetectionClass> detectionClasses, Size videoSize)
|
public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
|
||||||
{
|
{
|
||||||
foreach (var detection in detections)
|
foreach (var detection in annotation.Detections)
|
||||||
{
|
{
|
||||||
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
|
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
|
||||||
var canvasLabel = new CanvasLabel(detection, RenderSize, videoSize, detection.Confidence);
|
CanvasLabel canvasLabel;
|
||||||
CreateDetectionControl(detectionClass, time, canvasLabel);
|
if (!annotation.IsSplit || mediaSize.FitSizeForAI())
|
||||||
|
canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
canvasLabel = new CanvasLabel(detection, annotation.SplitTile!.Size, null, detection.Confidence)
|
||||||
|
.ReframeFromSmall(annotation.SplitTile);
|
||||||
|
|
||||||
|
//From CurrentMediaSize to Render Size
|
||||||
|
var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
|
||||||
|
canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
var control = CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
|
||||||
|
control.UpdateLayout();
|
||||||
|
CheckLabelBoundaries(control);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
|
private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
|
||||||
{
|
{
|
||||||
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
|
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
|
||||||
detectionControl.MouseDown += AnnotationPositionStart;
|
detectionControl.MouseDown += AnnotationPositionStart;
|
||||||
SetLeft(detectionControl, canvasLabel.X );
|
SetLeft(detectionControl, canvasLabel.Left );
|
||||||
SetTop(detectionControl, canvasLabel.Y);
|
SetTop(detectionControl, canvasLabel.Top);
|
||||||
Children.Add(detectionControl);
|
Children.Add(detectionControl);
|
||||||
CurrentDetections.Add(detectionControl);
|
CurrentDetections.Add(detectionControl);
|
||||||
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
|
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
|
||||||
|
return detectionControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -363,6 +541,12 @@ public class CanvasEditor : Canvas
|
|||||||
.ToList();
|
.ToList();
|
||||||
RemoveAnnotations(expiredAnns);
|
RemoveAnnotations(expiredAnns);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetBackground() => Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
|
public void ZoomTo(Point point)
|
||||||
|
{
|
||||||
|
SetZoom();
|
||||||
|
var matrix = _matrixTransform.Matrix;
|
||||||
|
matrix.ScaleAt(2, 2, point.X, point.Y);
|
||||||
|
SetZoom(matrix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
CanUserResizeColumns="False"
|
CanUserResizeColumns="False"
|
||||||
SelectionChanged="DetectionDataGrid_SelectionChanged"
|
SelectionChanged="DetectionDataGrid_SelectionChanged"
|
||||||
x:FieldModifier="public"
|
x:FieldModifier="public"
|
||||||
|
PreviewKeyDown="OnKeyBanActivity"
|
||||||
>
|
>
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
|
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
@@ -86,4 +87,11 @@ public partial class DetectionClasses
|
|||||||
{
|
{
|
||||||
DetectionDataGrid.SelectedIndex = keyNumber;
|
DetectionDataGrid.SelectedIndex = keyNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnKeyBanActivity(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,20 +5,21 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Label = System.Windows.Controls.Label;
|
using Annotation = Azaion.Common.Database.Annotation;
|
||||||
|
|
||||||
namespace Azaion.Common.Controls;
|
namespace Azaion.Common.Controls;
|
||||||
|
|
||||||
public class DetectionControl : Border
|
public class DetectionControl : Border
|
||||||
{
|
{
|
||||||
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
|
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
|
||||||
private const double RESIZE_RECT_SIZE = 12;
|
private const double RESIZE_RECT_SIZE = 10;
|
||||||
|
|
||||||
private readonly Grid _grid;
|
private readonly Grid _grid;
|
||||||
private readonly Label _detectionLabel;
|
private readonly DetectionLabelPanel _detectionLabelPanel;
|
||||||
|
public readonly Canvas DetectionLabelContainer;
|
||||||
|
|
||||||
public TimeSpan Time { get; set; }
|
public TimeSpan Time { get; set; }
|
||||||
private readonly double _confidence;
|
private readonly List<Rectangle> _resizedRectangles = new();
|
||||||
private List<Rectangle> _resizedRectangles = new();
|
|
||||||
|
|
||||||
private DetectionClass _detectionClass = null!;
|
private DetectionClass _detectionClass = null!;
|
||||||
public DetectionClass DetectionClass
|
public DetectionClass DetectionClass
|
||||||
@@ -28,12 +29,11 @@ public class DetectionControl : Border
|
|||||||
{
|
{
|
||||||
var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
|
var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
|
||||||
BorderBrush = brush;
|
BorderBrush = brush;
|
||||||
BorderThickness = new Thickness(3);
|
BorderThickness = new Thickness(1);
|
||||||
foreach (var rect in _resizedRectangles)
|
foreach (var rect in _resizedRectangles)
|
||||||
rect.Stroke = brush;
|
rect.Stroke = brush;
|
||||||
|
|
||||||
_detectionLabel.Background = new SolidColorBrush(value.Color.ToConfidenceColor(_confidence));
|
_detectionLabelPanel.DetectionClass = value;
|
||||||
_detectionLabel.Content = _detectionLabelText(value.UIName);
|
|
||||||
_detectionClass = value;
|
_detectionClass = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ public class DetectionControl : Border
|
|||||||
private readonly Rectangle _selectionFrame;
|
private readonly Rectangle _selectionFrame;
|
||||||
|
|
||||||
private bool _isSelected;
|
private bool _isSelected;
|
||||||
|
|
||||||
public bool IsSelected
|
public bool IsSelected
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _isSelected;
|
||||||
@@ -51,37 +52,56 @@ public class DetectionControl : Border
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _detectionLabelText(string detectionClassName) =>
|
public void UpdateAdornerScale(double scale)
|
||||||
_confidence >= 0.995 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%"; //double
|
{
|
||||||
|
if (Math.Abs(scale) < 0.0001)
|
||||||
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object, MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
|
return;
|
||||||
|
|
||||||
|
var inverseScale = 1.0 / scale;
|
||||||
|
BorderThickness = new Thickness(4 * inverseScale);
|
||||||
|
foreach (var rect in _resizedRectangles)
|
||||||
|
{
|
||||||
|
rect.Width = 2 * RESIZE_RECT_SIZE * inverseScale;
|
||||||
|
rect.Height = 2 * RESIZE_RECT_SIZE * inverseScale;;
|
||||||
|
rect.Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (HorizontalAlignment Horizontal, VerticalAlignment Vertical) DetectionLabelPosition
|
||||||
|
{
|
||||||
|
get => (DetectionLabelContainer.HorizontalAlignment, DetectionLabelContainer.VerticalAlignment);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
DetectionLabelContainer.HorizontalAlignment = value.Horizontal;
|
||||||
|
DetectionLabelContainer.VerticalAlignment = value.Vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object,
|
||||||
|
MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
|
||||||
{
|
{
|
||||||
Width = canvasLabel.Width;
|
Width = canvasLabel.Width;
|
||||||
Height = canvasLabel.Height;
|
Height = canvasLabel.Height;
|
||||||
Time = time;
|
Time = time;
|
||||||
_resizeStart = resizeStart;
|
_resizeStart = resizeStart;
|
||||||
_confidence = canvasLabel.Confidence;
|
|
||||||
|
DetectionLabelContainer = new Canvas
|
||||||
var labelContainer = new Canvas
|
|
||||||
{
|
{
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
ClipToBounds = false,
|
ClipToBounds = false,
|
||||||
Margin = new Thickness(0, 0, 32, 0)
|
|
||||||
};
|
};
|
||||||
_detectionLabel = new Label
|
_detectionLabelPanel = new DetectionLabelPanel
|
||||||
{
|
{
|
||||||
Content = _detectionLabelText(detectionClass.Name),
|
Confidence = canvasLabel.Confidence,
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
|
||||||
Margin = new Thickness(0, -32, 0, 0),
|
|
||||||
FontSize = 16,
|
|
||||||
Visibility = Visibility.Visible
|
|
||||||
};
|
};
|
||||||
labelContainer.Children.Add(_detectionLabel);
|
|
||||||
|
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
|
||||||
|
|
||||||
_selectionFrame = new Rectangle
|
_selectionFrame = new Rectangle
|
||||||
{
|
{
|
||||||
|
Margin = new Thickness(-3),
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
VerticalAlignment = VerticalAlignment.Stretch,
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
Stroke = new SolidColorBrush(Colors.Black),
|
Stroke = new SolidColorBrush(Colors.Black),
|
||||||
@@ -106,9 +126,9 @@ public class DetectionControl : Border
|
|||||||
VerticalAlignment = VerticalAlignment.Stretch,
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
Children = { _selectionFrame }
|
Children = { _selectionFrame }
|
||||||
};
|
};
|
||||||
|
_grid.Children.Add(DetectionLabelContainer);
|
||||||
foreach (var rect in _resizedRectangles)
|
foreach (var rect in _resizedRectangles)
|
||||||
_grid.Children.Add(rect);
|
_grid.Children.Add(rect);
|
||||||
_grid.Children.Add(labelContainer);
|
|
||||||
|
|
||||||
Child = _grid;
|
Child = _grid;
|
||||||
Cursor = Cursors.SizeAll;
|
Cursor = Cursors.SizeAll;
|
||||||
@@ -121,23 +141,25 @@ public class DetectionControl : Border
|
|||||||
var rect = new Rectangle() // small rectangles at the corners and sides
|
var rect = new Rectangle() // small rectangles at the corners and sides
|
||||||
{
|
{
|
||||||
ClipToBounds = false,
|
ClipToBounds = false,
|
||||||
Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7),
|
Margin = new Thickness(-1.1 * RESIZE_RECT_SIZE),
|
||||||
HorizontalAlignment = ha,
|
HorizontalAlignment = ha,
|
||||||
VerticalAlignment = va,
|
VerticalAlignment = va,
|
||||||
Width = RESIZE_RECT_SIZE,
|
Width = RESIZE_RECT_SIZE,
|
||||||
Height = RESIZE_RECT_SIZE,
|
Height = RESIZE_RECT_SIZE,
|
||||||
Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
|
Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
|
||||||
|
StrokeThickness = 0.8,
|
||||||
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
|
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
|
||||||
Cursor = crs,
|
Cursor = crs,
|
||||||
Name = name,
|
Name = name,
|
||||||
};
|
};
|
||||||
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
|
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
|
||||||
|
rect.MouseUp += (sender, args) => { (sender as UIElement)?.ReleaseMouseCapture(); };
|
||||||
return rect;
|
return rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
|
public CanvasLabel ToCanvasLabel() =>
|
||||||
{
|
new(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||||
var label = new CanvasLabel(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
|
||||||
return new YoloLabel(label, canvasSize, videoSize);
|
public YoloLabel ToYoloLabel(Size canvasSize, Size? videoSize = null) =>
|
||||||
}
|
new(ToCanvasLabel(), canvasSize, videoSize);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<UserControl x:Class="Azaion.Common.Controls.DetectionLabelPanel"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<!-- Friendly (Light Blue Square) -->
|
||||||
|
<DrawingImage x:Key="Friendly">
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightBlue" Geometry="M25,50 l150,0 0,100 -150,0 z">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="Black" Thickness="8"/>
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
|
||||||
|
<!-- Hostile (Red Diamond) -->
|
||||||
|
<DrawingImage x:Key="Hostile">
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="Red" Geometry="M 100,28 L172,100 100,172 28,100 100,28 Z">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="Black" Thickness="8"/>
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
|
||||||
|
<!-- Unknown (Yellow Quatrefoil) -->
|
||||||
|
<DrawingImage x:Key="Unknown">
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="Yellow" Geometry="M63,63 C63,20 137,20 137,63 C180,63 180,137 137,137 C137,180
|
||||||
|
63,180 63,137 C20,137 20,63 63,63 Z">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="Black" Thickness="8"/>
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid x:Name="DetectionGrid">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image Grid.Column="0" x:Name="AffiliationImage">
|
||||||
|
</Image>
|
||||||
|
<Label Grid.Column="1" x:Name="DetectionClassName" FontSize="16"></Label>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Controls
|
||||||
|
{
|
||||||
|
public partial class DetectionLabelPanel
|
||||||
|
{
|
||||||
|
private AffiliationEnum _affiliation = AffiliationEnum.None;
|
||||||
|
|
||||||
|
public AffiliationEnum Affiliation
|
||||||
|
{
|
||||||
|
get => _affiliation;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_affiliation = value;
|
||||||
|
UpdateAffiliationImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DetectionClass _detectionClass = new();
|
||||||
|
public DetectionClass DetectionClass {
|
||||||
|
get => _detectionClass;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_detectionClass = value;
|
||||||
|
SetClassName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _confidence;
|
||||||
|
public double Confidence
|
||||||
|
{
|
||||||
|
get => _confidence;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_confidence = value;
|
||||||
|
SetClassName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetClassName()
|
||||||
|
{
|
||||||
|
DetectionClassName.Content = _confidence >= 0.995 ? _detectionClass.UIName : $"{_detectionClass.UIName}: {_confidence * 100:F0}%";
|
||||||
|
DetectionGrid.Background = new SolidColorBrush(_detectionClass.Color.ToConfidenceColor(_confidence));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionLabelPanel()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _detectionLabelText(string detectionClassName) =>
|
||||||
|
_confidence >= 0.98 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%";
|
||||||
|
|
||||||
|
private void UpdateAffiliationImage()
|
||||||
|
{
|
||||||
|
if (_affiliation == AffiliationEnum.None)
|
||||||
|
{
|
||||||
|
AffiliationImage.Source = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFindResource(_affiliation.ToString()) is DrawingImage drawingImage)
|
||||||
|
AffiliationImage.Source = drawingImage;
|
||||||
|
else
|
||||||
|
AffiliationImage.Source = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<UserControl x:Class="Azaion.Common.Controls.NumericUpDown"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:Azaion.Common.Controls"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="300" d:DesignWidth="300">
|
||||||
|
<Grid Background="DimGray">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="24" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="12" />
|
||||||
|
<RowDefinition Height="12" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBox Name="NudTextBox"
|
||||||
|
Background="DimGray"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
TextAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
Text="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}"
|
||||||
|
LostFocus="NudTextBox_OnLostFocus"
|
||||||
|
PreviewTextInput="NudTextBox_OnPreviewTextInput"
|
||||||
|
DataObject.Pasting="NudTextBox_OnPasting"
|
||||||
|
/>
|
||||||
|
<RepeatButton
|
||||||
|
Name="NudButtonUp"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="0"
|
||||||
|
FontSize="10"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Click="NudButtonUp_OnClick"
|
||||||
|
>^</RepeatButton>
|
||||||
|
<RepeatButton
|
||||||
|
Name="NudButtonDown"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="1"
|
||||||
|
FontSize="10"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalContentAlignment="Center"
|
||||||
|
Click="NudButtonDown_OnClick"
|
||||||
|
>˅</RepeatButton>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Controls;
|
||||||
|
|
||||||
|
public partial class NumericUpDown : UserControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register(
|
||||||
|
nameof(MinValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(0m));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(
|
||||||
|
nameof(MaxValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(100m));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
|
||||||
|
nameof(Value), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m, OnValueChanged));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty StepProperty = DependencyProperty.Register(
|
||||||
|
nameof(Step), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m));
|
||||||
|
|
||||||
|
public decimal MinValue
|
||||||
|
{
|
||||||
|
get => (decimal)GetValue(MinValueProperty);
|
||||||
|
set => SetValue(MinValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal MaxValue
|
||||||
|
{
|
||||||
|
get => (decimal)GetValue(MaxValueProperty);
|
||||||
|
set => SetValue(MaxValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Value
|
||||||
|
{
|
||||||
|
get => (decimal)GetValue(ValueProperty);
|
||||||
|
set => SetValue(ValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal Step
|
||||||
|
{
|
||||||
|
get => (decimal)GetValue(StepProperty);
|
||||||
|
set => SetValue(StepProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumericUpDown()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (d is not NumericUpDown control)
|
||||||
|
return;
|
||||||
|
|
||||||
|
control.NudTextBox.Text = ((decimal)e.NewValue).ToString(CultureInfo.InvariantCulture);
|
||||||
|
control.NudTextBox.SelectionStart = control.NudTextBox.Text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NudButtonUp_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var step = Step <= 0 ? 1m : Step;
|
||||||
|
var newVal = Math.Min(MaxValue, Value + step);
|
||||||
|
Value = newVal;
|
||||||
|
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NudButtonDown_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var step = Step <= 0 ? 1m : Step;
|
||||||
|
var newVal = Math.Max(MinValue, Value - step);
|
||||||
|
Value = newVal;
|
||||||
|
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NudTextBox_OnLostFocus(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(NudTextBox.Text) || !decimal.TryParse(NudTextBox.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
|
||||||
|
{
|
||||||
|
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (number > MaxValue )
|
||||||
|
{
|
||||||
|
Value = MaxValue;
|
||||||
|
NudTextBox.Text = MaxValue.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
else if (number < MinValue)
|
||||||
|
{
|
||||||
|
Value = MinValue;
|
||||||
|
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Value = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
NudTextBox.SelectionStart = NudTextBox.Text.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NudTextBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
|
||||||
|
{
|
||||||
|
var regex = new Regex("[^0-9.]+");
|
||||||
|
e.Handled = regex.IsMatch(e.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NudTextBox_OnPasting(object sender, DataObjectPastingEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.DataObject.GetDataPresent(typeof(string)))
|
||||||
|
{
|
||||||
|
var text = (string)e.DataObject.GetData(typeof(string));
|
||||||
|
var regex = new Regex("[^0-9.]+");
|
||||||
|
if (regex.IsMatch(text))
|
||||||
|
{
|
||||||
|
e.CancelCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.CancelCommand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MediatR;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public enum AIAvailabilityEnum
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Downloading = 10,
|
||||||
|
Converting = 20,
|
||||||
|
Uploading = 30,
|
||||||
|
Enabled = 200,
|
||||||
|
Warning = 300,
|
||||||
|
Error = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AIAvailabilityStatusEvent : INotification
|
||||||
|
{
|
||||||
|
[Key("s")] public AIAvailabilityEnum Status { get; set; }
|
||||||
|
[Key("m")] public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => $"{StatusMessageDict.GetValueOrDefault(Status, "Помилка")} {ErrorMessage}";
|
||||||
|
|
||||||
|
private static readonly Dictionary<AIAvailabilityEnum, string> StatusMessageDict = new()
|
||||||
|
{
|
||||||
|
{ AIAvailabilityEnum.Downloading, "Йде завантаження AI для Вашої відеокарти" },
|
||||||
|
{ AIAvailabilityEnum.Converting, "Йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
|
||||||
|
{ AIAvailabilityEnum.Uploading, "Йде зберігання AI" },
|
||||||
|
{ AIAvailabilityEnum.Enabled, "AI готовий для розпізнавання" },
|
||||||
|
{ AIAvailabilityEnum.Warning, "Неможливо запустити AI наразі, йде налаштування під Ваше залізо" },
|
||||||
|
{ AIAvailabilityEnum.Error, "Помилка під час налаштування AI" }
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public enum AffiliationEnum
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Friendly = 10,
|
||||||
|
Hostile = 20,
|
||||||
|
Unknown = 30
|
||||||
|
}
|
||||||
@@ -3,31 +3,33 @@ using Azaion.Common.Database;
|
|||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class AnnotationResult
|
// public class AnnotationResult
|
||||||
{
|
//{
|
||||||
public Annotation Annotation { get; set; }
|
//public Annotation Annotation { get; set; }
|
||||||
public List<(Color Color, double Confidence)> Colors { get; private set; }
|
|
||||||
|
|
||||||
public string ImagePath { get; set; }
|
//public string ImagePath { get; set; }
|
||||||
public string TimeStr { get; set; }
|
//public string TimeStr { get; set; }
|
||||||
public string ClassName { get; set; }
|
|
||||||
|
//public List<(Color Color, double Confidence)> Colors { get; private set; }
|
||||||
|
// public string ClassName { get; set; }
|
||||||
|
|
||||||
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
|
// public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
|
||||||
{
|
// {
|
||||||
|
|
||||||
Annotation = annotation;
|
//Annotation = annotation;
|
||||||
|
|
||||||
TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
|
//TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
|
||||||
ImagePath = annotation.ImagePath;
|
//ImagePath = annotation.ImagePath;
|
||||||
|
|
||||||
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
// var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||||
|
// ClassName = detectionClasses.Count > 1
|
||||||
Colors = annotation.Detections
|
// ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
|
||||||
.Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
|
// : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
||||||
.ToList();
|
//
|
||||||
|
// Colors = annotation.Detections
|
||||||
ClassName = detectionClasses.Count > 1
|
// .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
|
||||||
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
|
// .ToList();
|
||||||
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
@@ -7,9 +7,10 @@ using Azaion.Common.Extensions;
|
|||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
|
public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public Annotation Annotation { get; set; } = annotation;
|
public Annotation Annotation { get; set; } = annotation;
|
||||||
|
public bool IsValidator { get; set; } = isValidator;
|
||||||
|
|
||||||
private BitmapImage? _thumbnail;
|
private BitmapImage? _thumbnail;
|
||||||
public BitmapImage? Thumbnail
|
public BitmapImage? Thumbnail
|
||||||
@@ -28,7 +29,11 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
|
|
||||||
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
||||||
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
|
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
|
||||||
|
public string CreatedEmail => Annotation.CreatedEmail;
|
||||||
|
public bool IsSeed => IsValidator &&
|
||||||
|
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
|
||||||
|
!Annotation.CreatedRole.IsValidator();
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using CommandLine;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
[Verb("credsManual", HelpText = "Manual Credentials")]
|
||||||
|
public class ApiCredentials
|
||||||
|
{
|
||||||
|
[Key(nameof(Email))]
|
||||||
|
[Option('e', "email", Required = true, HelpText = "User Email")]
|
||||||
|
public string Email { get; set; } = null!;
|
||||||
|
|
||||||
|
[Key(nameof(Password))]
|
||||||
|
[Option('p', "pass", Required = true, HelpText = "User Password")]
|
||||||
|
public string Password { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Verb("credsEncrypted", isDefault: true, HelpText = "Encrypted Credentials")]
|
||||||
|
public class ApiCredentialsEncrypted
|
||||||
|
{
|
||||||
|
[Option('c', "creds", Group = "auto", HelpText = "Encrypted Creds")]
|
||||||
|
public string Creds { get; set; } = null!;
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
internal class BusinessExceptionDto
|
public class BusinessExceptionDto
|
||||||
{
|
{
|
||||||
public int ErrorCode { get; set; }
|
public int ErrorCode { get; set; }
|
||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class ClusterDistribution
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public Color Color { get; set; }
|
||||||
|
public int ClassCount { get; set; }
|
||||||
|
public double BarWidth { get; set; }
|
||||||
|
}
|
||||||
@@ -15,5 +15,11 @@ public class AIRecognitionConfig
|
|||||||
|
|
||||||
[Key("d")] public byte[] Data { get; set; } = null!;
|
[Key("d")] public byte[] Data { get; set; } = null!;
|
||||||
[Key("p")] public List<string> Paths { get; set; } = null!;
|
[Key("p")] public List<string> Paths { get; set; } = null!;
|
||||||
[Key("m_bs")] public int ModelBatchSize { get; set; } = 2;
|
[Key("m_bs")] public int ModelBatchSize { get; set; } = 4;
|
||||||
|
|
||||||
|
[Key("ov_p")] public double BigImageTileOverlapPercent { get; set; }
|
||||||
|
|
||||||
|
[Key("cam_a")] public double Altitude { get; set; }
|
||||||
|
[Key("cam_fl")] public double CameraFocalLength { get; set; }
|
||||||
|
[Key("cam_sw")] public double CameraSensorWidth { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Azaion.CommonSecurity;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
public class AppConfig
|
public class AppConfig
|
||||||
{
|
{
|
||||||
|
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
@@ -25,6 +26,10 @@ public class AppConfig
|
|||||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||||
|
|
||||||
public MapConfig MapConfig{ get; set; } = null!;
|
public MapConfig MapConfig{ get; set; } = null!;
|
||||||
|
|
||||||
|
public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public CameraConfig CameraConfig { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConfigUpdater
|
public interface IConfigUpdater
|
||||||
@@ -35,53 +40,35 @@ public interface IConfigUpdater
|
|||||||
|
|
||||||
public class ConfigUpdater : IConfigUpdater
|
public class ConfigUpdater : IConfigUpdater
|
||||||
{
|
{
|
||||||
|
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
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, SecurityConstants.CONFIG_PATH);
|
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
|
||||||
|
|
||||||
if (File.Exists(configFilePath))
|
if (File.Exists(configFilePath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var appConfig = new AppConfig
|
Save(Constants.FailsafeAppConfig);
|
||||||
{
|
|
||||||
AnnotationConfig = Constants.DefaultAnnotationConfig,
|
|
||||||
|
|
||||||
UIConfig = new UIConfig
|
|
||||||
{
|
|
||||||
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
|
||||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
|
||||||
GenerateAnnotatedImage = false
|
|
||||||
},
|
|
||||||
|
|
||||||
DirectoriesConfig = new DirectoriesConfig
|
|
||||||
{
|
|
||||||
VideosDirectory = Constants.DEFAULT_VIDEO_DIR,
|
|
||||||
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
|
|
||||||
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
|
|
||||||
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
|
|
||||||
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
|
|
||||||
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
|
|
||||||
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
|
|
||||||
},
|
|
||||||
|
|
||||||
ThumbnailConfig = Constants.DefaultThumbnailConfig,
|
|
||||||
AIRecognitionConfig = Constants.DefaultAIRecognitionConfig
|
|
||||||
};
|
|
||||||
Save(appConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(AppConfig config)
|
public void Save(AppConfig config)
|
||||||
{
|
{
|
||||||
//Save only user's config
|
ThrottleExt.Throttle(async () =>
|
||||||
var publicConfig = new
|
|
||||||
{
|
{
|
||||||
config.InferenceClientConfig,
|
var publicConfig = new
|
||||||
config.GpsDeniedClientConfig,
|
{
|
||||||
config.DirectoriesConfig,
|
config.LoaderClientConfig,
|
||||||
config.UIConfig
|
config.InferenceClientConfig,
|
||||||
};
|
config.GpsDeniedClientConfig,
|
||||||
|
config.DirectoriesConfig,
|
||||||
|
config.UIConfig,
|
||||||
|
config.CameraConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
|
||||||
|
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
public class CameraConfig
|
||||||
|
{
|
||||||
|
public decimal Altitude { get; set; }
|
||||||
|
public decimal CameraFocalLength { get; set; }
|
||||||
|
public decimal CameraSensorWidth { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
public class GpsDeniedConfig
|
||||||
|
{
|
||||||
|
public int MinKeyPoints { get; set; }
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ public class UIConfig
|
|||||||
public double RightPanelWidth { get; set; }
|
public double RightPanelWidth { get; set; }
|
||||||
public bool GenerateAnnotatedImage { get; set; }
|
public bool GenerateAnnotatedImage { get; set; }
|
||||||
public bool SilentDetection { get; set; }
|
public bool SilentDetection { get; set; }
|
||||||
|
public bool ShowDatasetWithDetectionsOnly { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class GeoPoint
|
||||||
|
{
|
||||||
|
const double PRECISION_TOLERANCE = 0.00005;
|
||||||
|
public double Lat { get; }
|
||||||
|
public double Lon { get; }
|
||||||
|
|
||||||
|
public GeoPoint() { }
|
||||||
|
|
||||||
|
public GeoPoint(double lat, double lon)
|
||||||
|
{
|
||||||
|
Lat = lat;
|
||||||
|
Lon = lon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{Lat:F4}, {Lon:F4}";
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not GeoPoint point) return false;
|
||||||
|
return ReferenceEquals(this, obj) || Equals(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Equals(GeoPoint point) =>
|
||||||
|
Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE;
|
||||||
|
|
||||||
|
public override int GetHashCode() => HashCode.Combine(Lat, Lon);
|
||||||
|
|
||||||
|
public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right);
|
||||||
|
public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right);
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ public class DetectionClass : ICloneable
|
|||||||
|
|
||||||
public Color Color { get; set; }
|
public Color Color { get; set; }
|
||||||
|
|
||||||
|
public int MaxSizeM { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string UIName
|
public string UIName
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class Direction
|
||||||
|
{
|
||||||
|
public double Distance { get; set; }
|
||||||
|
public double Azimuth { get; set; }
|
||||||
|
|
||||||
|
public Direction() { }
|
||||||
|
|
||||||
|
public Direction(double distance, double azimuth)
|
||||||
|
{
|
||||||
|
Distance = distance;
|
||||||
|
Azimuth = azimuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg";
|
||||||
|
}
|
||||||
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class DirectoriesConfig
|
public class DirectoriesConfig
|
||||||
|
|
||||||
{
|
{
|
||||||
public string ApiResourcesDirectory { get; set; } = null!;
|
public string? ApiResourcesDirectory { get; set; } = null!;
|
||||||
|
|
||||||
public string VideosDirectory { get; set; } = null!;
|
public string VideosDirectory { get; set; } = null!;
|
||||||
public string LabelsDirectory { get; set; } = null!;
|
public string LabelsDirectory { get; set; } = null!;
|
||||||
+8
-5
@@ -1,19 +1,22 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public abstract class ExternalClientConfig
|
public abstract class ExternalClientConfig
|
||||||
{
|
{
|
||||||
public string ZeroMqHost { get; set; } = "";
|
public string ZeroMqHost { get; set; } = "";
|
||||||
public int ZeroMqPort { get; set; }
|
public int ZeroMqPort { get; set; }
|
||||||
public double OneTryTimeoutSeconds { get; set; }
|
}
|
||||||
public int RetryCount {get;set;}
|
|
||||||
|
public class LoaderClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ApiUrl { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class InferenceClientConfig : ExternalClientConfig
|
public class InferenceClientConfig : ExternalClientConfig
|
||||||
{
|
{
|
||||||
public string ApiUrl { get; set; }
|
public string ApiUrl { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GpsDeniedClientConfig : ExternalClientConfig
|
public class GpsDeniedClientConfig : ExternalClientConfig
|
||||||
{
|
{
|
||||||
public int ZeroMqSubscriberPort { get; set; }
|
public int ZeroMqReceiverPort { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class FormState
|
public class FormState
|
||||||
{
|
{
|
||||||
public MediaFileInfo? CurrentMedia { get; set; }
|
public MediaFileInfo? CurrentMedia { get; set; }
|
||||||
public string VideoName => CurrentMedia?.FName ?? "";
|
public string MediaName => CurrentMedia?.FName ?? "";
|
||||||
|
|
||||||
public string CurrentMrl { get; set; } = null!;
|
public Size CurrentMediaSize { get; set; }
|
||||||
public Size CurrentVideoSize { get; set; }
|
|
||||||
public TimeSpan CurrentVideoLength { get; set; }
|
public TimeSpan CurrentVideoLength { get; set; }
|
||||||
|
|
||||||
public TimeSpan? BackgroundTime { get; set; }
|
public TimeSpan? BackgroundTime { get; set; }
|
||||||
public int CurrentVolume { get; set; } = 100;
|
public int CurrentVolume { get; set; } = 100;
|
||||||
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
|
public ObservableCollection<Annotation> AnnotationResults { get; set; } = [];
|
||||||
public WindowEnum ActiveWindow { get; set; }
|
public WindowEnum ActiveWindow { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
namespace Azaion.Common.DTO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
public class GpsMatchResult
|
|
||||||
{
|
|
||||||
public int Index { get; set; }
|
|
||||||
public string Image { get; set; } = null!;
|
|
||||||
public double Latitude { get; set; }
|
|
||||||
public double Longitude { get; set; }
|
|
||||||
public int KeyPoints { get; set; }
|
|
||||||
public int Rotation { get; set; }
|
|
||||||
public string MatchType { get; set; } = null!;
|
|
||||||
|
|
||||||
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
|
|
||||||
{
|
|
||||||
var imageDatas = new List<GpsMatchResult>();
|
|
||||||
|
|
||||||
using var reader = new StreamReader(csvFilePath);
|
|
||||||
//read header
|
|
||||||
reader.ReadLine();
|
|
||||||
if (reader.EndOfStream)
|
|
||||||
return new List<GpsMatchResult>();
|
|
||||||
|
|
||||||
while (!reader.EndOfStream)
|
|
||||||
{
|
|
||||||
var line = reader.ReadLine();
|
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
|
||||||
continue;
|
|
||||||
var values = line.Split(',');
|
|
||||||
if (values.Length == 6)
|
|
||||||
{
|
|
||||||
imageDatas.Add(new GpsMatchResult
|
|
||||||
{
|
|
||||||
Image = GetFilename(values[0]),
|
|
||||||
Latitude = double.Parse(values[1]),
|
|
||||||
Longitude = double.Parse(values[2]),
|
|
||||||
KeyPoints = int.Parse(values[3]),
|
|
||||||
Rotation = int.Parse(values[4]),
|
|
||||||
MatchType = values[5]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageDatas;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetFilename(string imagePath) =>
|
|
||||||
Path.GetFileNameWithoutExtension(imagePath)
|
|
||||||
.Replace("-small", "");
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public static class EnumerableExtensions
|
public static class EnumerableExtensions
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
using Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
public class SecureAppConfig
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class InitConfig
|
||||||
{
|
{
|
||||||
|
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||||
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||||
|
public CameraConfig CameraConfig { get; set; } = null!;
|
||||||
}
|
}
|
||||||
+55
-42
@@ -22,32 +22,56 @@ public abstract class Label
|
|||||||
|
|
||||||
public class CanvasLabel : Label
|
public class CanvasLabel : Label
|
||||||
{
|
{
|
||||||
public double X { get; set; }
|
public double Left { get; set; }
|
||||||
public double Y { get; set; }
|
public double Top { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Height { get; set; }
|
public double Height { get; set; }
|
||||||
public double Confidence { get; set; }
|
public double Confidence { get; set; }
|
||||||
|
|
||||||
public CanvasLabel()
|
public double Bottom
|
||||||
{
|
{
|
||||||
|
get => Top + Height;
|
||||||
|
set => Height = value - Top;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CanvasLabel(int classNumber, double x, double y, double width, double height, double confidence = 1) : base(classNumber)
|
public double Right
|
||||||
{
|
{
|
||||||
X = x;
|
get => Left + Width;
|
||||||
Y = y;
|
set => Width = value - Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double CenterX => Left + Width / 2.0;
|
||||||
|
public double CenterY => Top + Height / 2.0;
|
||||||
|
public Size Size => new(Width, Height);
|
||||||
|
|
||||||
|
public CanvasLabel() { }
|
||||||
|
|
||||||
|
public CanvasLabel(double left, double right, double top, double bottom)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Top = top;
|
||||||
|
Width = right - left;
|
||||||
|
Height = bottom - top;
|
||||||
|
Confidence = 1;
|
||||||
|
ClassNumber = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CanvasLabel(int classNumber, double left, double top, double width, double height, double confidence = 1) : base(classNumber)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Top = top;
|
||||||
Width = width;
|
Width = width;
|
||||||
Height = height;
|
Height = height;
|
||||||
Confidence = confidence;
|
Confidence = confidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double confidence = 1)
|
public CanvasLabel(YoloLabel label, Size canvasSize, Size? mediaSize = null, double confidence = 1)
|
||||||
{
|
{
|
||||||
var cw = canvasSize.Width;
|
var cw = canvasSize.Width;
|
||||||
var ch = canvasSize.Height;
|
var ch = canvasSize.Height;
|
||||||
var canvasAr = cw / ch;
|
var canvasAr = cw / ch;
|
||||||
var videoAr = videoSize.HasValue
|
var videoAr = mediaSize.HasValue
|
||||||
? videoSize.Value.Width / videoSize.Value.Height
|
? mediaSize.Value.Width / mediaSize.Value.Height
|
||||||
: canvasAr;
|
: canvasAr;
|
||||||
|
|
||||||
ClassNumber = label.ClassNumber;
|
ClassNumber = label.ClassNumber;
|
||||||
@@ -60,8 +84,8 @@ public class CanvasLabel : Label
|
|||||||
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
||||||
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
||||||
|
|
||||||
X = left * cw;
|
Left = left * cw;
|
||||||
Y = top * realHeight + blackStripHeight;
|
Top = top * realHeight + blackStripHeight;
|
||||||
Width = label.Width * cw;
|
Width = label.Width * cw;
|
||||||
Height = label.Height * realHeight;
|
Height = label.Height * realHeight;
|
||||||
}
|
}
|
||||||
@@ -70,13 +94,20 @@ public class CanvasLabel : Label
|
|||||||
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
||||||
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
||||||
|
|
||||||
X = left * realWidth + blackStripWidth;
|
Left = left * realWidth + blackStripWidth;
|
||||||
Y = top * ch;
|
Top = top * ch;
|
||||||
Width = label.Width * realWidth;
|
Width = label.Width * realWidth;
|
||||||
Height = label.Height * ch;
|
Height = label.Height * ch;
|
||||||
}
|
}
|
||||||
Confidence = confidence;
|
Confidence = confidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CanvasLabel ReframeToSmall(CanvasLabel smallTile) =>
|
||||||
|
new(ClassNumber, Left - smallTile.Left, Top - smallTile.Top, Width, Height, Confidence);
|
||||||
|
|
||||||
|
public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) =>
|
||||||
|
new(ClassNumber, Left + smallTile.Left, Top + smallTile.Top, Width, Height, Confidence);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
@@ -105,13 +136,13 @@ public class YoloLabel : Label
|
|||||||
public RectangleF ToRectangle() =>
|
public RectangleF ToRectangle() =>
|
||||||
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
|
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
|
||||||
|
|
||||||
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null)
|
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? mediaSize = null)
|
||||||
{
|
{
|
||||||
var cw = canvasSize.Width;
|
var cw = canvasSize.Width;
|
||||||
var ch = canvasSize.Height;
|
var ch = canvasSize.Height;
|
||||||
var canvasAr = cw / ch;
|
var canvasAr = cw / ch;
|
||||||
var videoAr = videoSize.HasValue
|
var videoAr = mediaSize.HasValue
|
||||||
? videoSize.Value.Width / videoSize.Value.Height
|
? mediaSize.Value.Width / mediaSize.Value.Height
|
||||||
: canvasAr;
|
: canvasAr;
|
||||||
|
|
||||||
ClassNumber = canvasLabel.ClassNumber;
|
ClassNumber = canvasLabel.ClassNumber;
|
||||||
@@ -119,20 +150,20 @@ public class YoloLabel : Label
|
|||||||
double left, top;
|
double left, top;
|
||||||
if (videoAr > canvasAr) //100% width
|
if (videoAr > canvasAr) //100% width
|
||||||
{
|
{
|
||||||
left = canvasLabel.X / cw;
|
left = canvasLabel.Left / cw;
|
||||||
Width = canvasLabel.Width / cw;
|
Width = canvasLabel.Width / cw;
|
||||||
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
var realHeight = cw / videoAr; //real video height in pixels on canvas
|
||||||
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
|
||||||
top = (canvasLabel.Y - blackStripHeight) / realHeight;
|
top = (canvasLabel.Top - blackStripHeight) / realHeight;
|
||||||
Height = canvasLabel.Height / realHeight;
|
Height = canvasLabel.Height / realHeight;
|
||||||
}
|
}
|
||||||
else //100% height
|
else //100% height
|
||||||
{
|
{
|
||||||
top = canvasLabel.Y / ch;
|
top = canvasLabel.Top / ch;
|
||||||
Height = canvasLabel.Height / ch;
|
Height = canvasLabel.Height / ch;
|
||||||
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
var realWidth = ch * videoAr; //real video width in pixels on canvas
|
||||||
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
|
||||||
left = (canvasLabel.X - blackStripWidth) / realWidth;
|
left = (canvasLabel.Left - blackStripWidth) / realWidth;
|
||||||
Width = canvasLabel.Width / realWidth;
|
Width = canvasLabel.Width / realWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +177,11 @@ public class YoloLabel : Label
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var strings = s.Replace(',', '.').Split(' ');
|
var strings = s.Replace(',', '.').Split(' ');
|
||||||
if (strings.Length != 5)
|
if (strings.Length < 5)
|
||||||
throw new Exception("Wrong labels format!");
|
throw new Exception("Wrong labels format!");
|
||||||
|
if (strings.Length > 5)
|
||||||
|
strings = strings[..5];
|
||||||
|
|
||||||
|
|
||||||
var res = new YoloLabel
|
var res = new YoloLabel
|
||||||
{
|
{
|
||||||
@@ -184,24 +218,3 @@ public class YoloLabel : Label
|
|||||||
|
|
||||||
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(',', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public class Detection : YoloLabel
|
|
||||||
{
|
|
||||||
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
|
|
||||||
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
|
|
||||||
|
|
||||||
//For db & serialization
|
|
||||||
public Detection(){}
|
|
||||||
|
|
||||||
public Detection(string annotationName, YoloLabel label, double confidence = 1)
|
|
||||||
{
|
|
||||||
AnnotationName = annotationName;
|
|
||||||
ClassNumber = label.ClassNumber;
|
|
||||||
CenterX = label.CenterX;
|
|
||||||
CenterY = label.CenterY;
|
|
||||||
Height = label.Height;
|
|
||||||
Width = label.Width;
|
|
||||||
Confidence = confidence;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class LoginResponse
|
public class LoginResponse
|
||||||
{
|
{
|
||||||
@@ -1,27 +1,29 @@
|
|||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Queue;
|
namespace Azaion.Common.DTO.Queue;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class AnnotationCreatedMessage
|
public class AnnotationMessage
|
||||||
{
|
{
|
||||||
[Key(0)] public DateTime CreatedDate { get; set; }
|
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||||
[Key(1)] public string Name { get; set; } = null!;
|
[Key(1)] public string Name { get; set; } = null!;
|
||||||
[Key(2)] public string OriginalMediaName { get; set; } = null!;
|
[Key(2)] public string OriginalMediaName { get; set; } = null!;
|
||||||
[Key(3)] public TimeSpan Time { get; set; }
|
[Key(3)] public TimeSpan Time { get; set; }
|
||||||
[Key(4)] public string ImageExtension { get; set; } = null!;
|
[Key(4)] public string ImageExtension { get; set; } = null!;
|
||||||
[Key(5)] public string Detections { get; set; } = null!;
|
[Key(5)] public string Detections { get; set; } = null!;
|
||||||
[Key(6)] public byte[] Image { get; set; } = null!;
|
[Key(6)] public byte[]? Image { get; set; } = null!;
|
||||||
[Key(7)] public RoleEnum CreatedRole { get; set; }
|
[Key(7)] public RoleEnum Role { get; set; }
|
||||||
[Key(8)] public string CreatedEmail { get; set; } = null!;
|
[Key(8)] public string Email { get; set; } = null!;
|
||||||
[Key(9)] public SourceEnum Source { get; set; }
|
[Key(9)] public SourceEnum Source { get; set; }
|
||||||
[Key(10)] public AnnotationStatus Status { get; set; }
|
[Key(10)] public AnnotationStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class AnnotationValidatedMessage
|
public class AnnotationBulkMessage
|
||||||
{
|
{
|
||||||
[Key(0)] public string Name { get; set; } = null!;
|
[Key(0)] public string[] AnnotationNames { get; set; } = null!;
|
||||||
|
[Key(1)] public AnnotationStatus AnnotationStatus { get; set; }
|
||||||
|
[Key(2)] public string Email { get; set; } = null!;
|
||||||
|
[Key(3)] public DateTime CreatedDate { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
|
||||||
|
{
|
||||||
|
[Key("CommandType")]
|
||||||
|
public CommandType CommandType { get; set; } = commandType;
|
||||||
|
|
||||||
|
[Key("Data")]
|
||||||
|
public byte[]? Data { get; set; } = data;
|
||||||
|
|
||||||
|
[Key("Message")]
|
||||||
|
public string? Message { get; set; } = message;
|
||||||
|
|
||||||
|
public static RemoteCommand Create(CommandType commandType) =>
|
||||||
|
new(commandType);
|
||||||
|
|
||||||
|
public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
|
||||||
|
new(commandType, MessagePackSerializer.Serialize(data), message);
|
||||||
|
|
||||||
|
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class LoadFileData(string filename, string? folder = null )
|
||||||
|
{
|
||||||
|
[Key(nameof(Folder))]
|
||||||
|
public string? Folder { get; set; } = folder;
|
||||||
|
|
||||||
|
[Key(nameof(Filename))]
|
||||||
|
public string Filename { get; set; } = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum CommandType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Ok = 3,
|
||||||
|
Login = 10,
|
||||||
|
CheckResource = 12,
|
||||||
|
ListRequest = 15,
|
||||||
|
ListFiles = 18,
|
||||||
|
Load = 20,
|
||||||
|
LoadBigSmall = 22,
|
||||||
|
UploadBigSmall = 24,
|
||||||
|
DataBytes = 25,
|
||||||
|
Inference = 30,
|
||||||
|
InferenceData = 35,
|
||||||
|
InferenceStatus = 37,
|
||||||
|
InferenceDone = 38,
|
||||||
|
StopInference = 40,
|
||||||
|
AIAvailabilityCheck = 80,
|
||||||
|
AIAvailabilityResult = 85,
|
||||||
|
Error = 90,
|
||||||
|
Exit = 100,
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using Azaion.Common.Extensions;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
public enum RoleEnum
|
public enum RoleEnum
|
||||||
{
|
{
|
||||||
@@ -6,11 +6,8 @@ public class SatTile
|
|||||||
{
|
{
|
||||||
public int X { get; }
|
public int X { get; }
|
||||||
public int Y { get; }
|
public int Y { get; }
|
||||||
public double LeftTopLat { get; }
|
public GeoPoint LeftTop { get; }
|
||||||
public double LeftTopLon { get; }
|
public GeoPoint BottomRight { get; }
|
||||||
|
|
||||||
public double BottomRightLat { get; }
|
|
||||||
public double BottomRightLon { get; }
|
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
|
||||||
@@ -20,12 +17,12 @@ public class SatTile
|
|||||||
Y = y;
|
Y = y;
|
||||||
Url = url;
|
Url = url;
|
||||||
|
|
||||||
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
|
LeftTop = GeoUtils.TileToWorldPos(x, y, zoom);
|
||||||
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
|
BottomRight = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
|
return $"Tile[X={X}, Y={Y}, TL=({LeftTop.Lat:F6}, {LeftTop.Lon:F6}), BR=({BottomRight.Lat:F6}, {BottomRight.Lon:F6})]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
namespace Azaion.Annotator.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public enum SelectionState
|
public enum SelectionState
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
NewAnnCreating = 1,
|
NewAnnCreating = 1,
|
||||||
AnnResizing = 2,
|
AnnResizing = 2,
|
||||||
AnnMoving = 3
|
AnnMoving = 3,
|
||||||
|
PanZoomMoving = 4,
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
|
||||||
namespace Azaion.Common.Database;
|
namespace Azaion.Common.Database;
|
||||||
@@ -13,12 +12,14 @@ public class Annotation
|
|||||||
private static string _labelsDir = null!;
|
private static string _labelsDir = null!;
|
||||||
private static string _imagesDir = null!;
|
private static string _imagesDir = null!;
|
||||||
private static string _thumbDir = null!;
|
private static string _thumbDir = null!;
|
||||||
|
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
|
||||||
public static void InitializeDirs(DirectoriesConfig config)
|
|
||||||
|
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
|
||||||
{
|
{
|
||||||
_labelsDir = config.LabelsDirectory;
|
_labelsDir = config.LabelsDirectory;
|
||||||
_imagesDir = config.ImagesDirectory;
|
_imagesDir = config.ImagesDirectory;
|
||||||
_thumbDir = config.ThumbnailsDirectory;
|
_thumbDir = config.ThumbnailsDirectory;
|
||||||
|
DetectionClassesDict = detectionClassesDict;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Key("n")] public string Name { get; set; } = null!;
|
[Key("n")] public string Name { get; set; } = null!;
|
||||||
@@ -41,12 +42,64 @@ public class Annotation
|
|||||||
[Key("lon")]public double Lon { get; set; }
|
[Key("lon")]public double Lon { get; set; }
|
||||||
|
|
||||||
#region Calculated
|
#region Calculated
|
||||||
[IgnoreMember]public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
||||||
[IgnoreMember]public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
||||||
[IgnoreMember]public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||||
[IgnoreMember]public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
[IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
||||||
|
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
|
||||||
|
|
||||||
|
private CanvasLabel? _splitTile;
|
||||||
|
[IgnoreMember] public CanvasLabel? SplitTile
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!IsSplit)
|
||||||
|
return null;
|
||||||
|
if (_splitTile != null)
|
||||||
|
return _splitTile;
|
||||||
|
|
||||||
|
var startCoordIndex = Name.IndexOf(Constants.SPLIT_SUFFIX, StringComparison.Ordinal) + Constants.SPLIT_SUFFIX.Length;
|
||||||
|
var coordsStr = Name.Substring(startCoordIndex, 14).Split('_');
|
||||||
|
_splitTile = new CanvasLabel
|
||||||
|
{
|
||||||
|
Left = double.Parse(coordsStr[1]),
|
||||||
|
Top = double.Parse(coordsStr[2]),
|
||||||
|
Width = double.Parse(coordsStr[0]),
|
||||||
|
Height = double.Parse(coordsStr[0])
|
||||||
|
};
|
||||||
|
return _splitTile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
||||||
|
|
||||||
|
private List<(Color Color, double Confidence)>? _colors;
|
||||||
|
[IgnoreMember] public List<(Color Color, double Confidence)> Colors => _colors ??= Detections
|
||||||
|
.Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private string? _className;
|
||||||
|
[IgnoreMember] public string ClassName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_className))
|
||||||
|
{
|
||||||
|
var detectionClasses = Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||||
|
_className = detectionClasses.Count > 1
|
||||||
|
? string.Join(", ", detectionClasses.Select(x => DetectionClassesDict[x].UIName))
|
||||||
|
: DetectionClassesDict[detectionClasses.FirstOrDefault()].UIName;
|
||||||
|
}
|
||||||
|
return _className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion Calculated
|
#endregion Calculated
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
@@ -59,5 +112,7 @@ public enum AnnotationStatus
|
|||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
Created = 10,
|
Created = 10,
|
||||||
Validated = 20
|
Edited = 20,
|
||||||
|
Validated = 30,
|
||||||
|
Deleted = 40
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Azaion.Common.Database;
|
|
||||||
|
|
||||||
public class AnnotationName
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class AnnotationQueueRecord
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public DateTime DateTime { get; set; }
|
||||||
|
public AnnotationStatus Operation { get; set; }
|
||||||
|
public List<string> AnnotationNames { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using CsvHelper;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ namespace Azaion.Common.Database;
|
|||||||
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
|
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
|
||||||
{
|
{
|
||||||
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||||
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
|
public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
|
||||||
public ITable<Detection> Detections => this.GetTable<Detection>();
|
public ITable<Detection> Detections => this.GetTable<Detection>();
|
||||||
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
|
public ITable<MediaFile> MediaFiles => this.GetTable<MediaFile>();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.Mapping;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public static class AnnotationsDbSchemaHolder
|
||||||
|
{
|
||||||
|
public static readonly MappingSchema MappingSchema;
|
||||||
|
|
||||||
|
static AnnotationsDbSchemaHolder()
|
||||||
|
{
|
||||||
|
MappingSchema = new MappingSchema();
|
||||||
|
var builder = new FluentMappingBuilder(MappingSchema);
|
||||||
|
|
||||||
|
var annotationBuilder = builder.Entity<Annotation>();
|
||||||
|
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
||||||
|
.HasPrimaryKey(x => x.Name)
|
||||||
|
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
|
||||||
|
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
|
||||||
|
|
||||||
|
annotationBuilder
|
||||||
|
.Ignore(x => x.Milliseconds)
|
||||||
|
.Ignore(x => x.Classes)
|
||||||
|
.Ignore(x => x.Classes)
|
||||||
|
.Ignore(x => x.ImagePath)
|
||||||
|
.Ignore(x => x.LabelPath)
|
||||||
|
.Ignore(x => x.ThumbPath);
|
||||||
|
|
||||||
|
builder.Entity<Detection>()
|
||||||
|
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
||||||
|
|
||||||
|
builder.Entity<AnnotationQueueRecord>()
|
||||||
|
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
|
||||||
|
.HasPrimaryKey(x => x.Id)
|
||||||
|
.Property(x => x.AnnotationNames)
|
||||||
|
.HasDataType(DataType.NVarChar)
|
||||||
|
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
|
||||||
|
|
||||||
|
builder.Entity<MediaFile>()
|
||||||
|
.HasTableName(Constants.MEDIAFILE_TABLENAME);
|
||||||
|
|
||||||
|
builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,26 +3,26 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
|
||||||
using LinqToDB.DataProvider.SQLite;
|
using LinqToDB.DataProvider.SQLite;
|
||||||
using LinqToDB.Mapping;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Azaion.Common.Database;
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
public interface IDbFactory
|
public interface IDbFactory
|
||||||
{
|
{
|
||||||
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
||||||
Task Run(Func<AnnotationsDb, Task> func);
|
Task RunWrite(Func<AnnotationsDb, Task> func);
|
||||||
void SaveToDisk();
|
Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func);
|
||||||
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
|
|
||||||
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
|
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DbFactory : IDbFactory
|
public class DbFactory : IDbFactory
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<DbFactory> _logger;
|
||||||
private readonly AnnotationConfig _annConfig;
|
private readonly AnnotationConfig _annConfig;
|
||||||
|
|
||||||
private string MemoryConnStr => "Data Source=:memory:";
|
private string MemoryConnStr => "Data Source=:memory:";
|
||||||
@@ -33,8 +33,12 @@ public class DbFactory : IDbFactory
|
|||||||
private readonly SQLiteConnection _fileConnection;
|
private readonly SQLiteConnection _fileConnection;
|
||||||
private readonly DataOptions _fileDataOptions;
|
private readonly DataOptions _fileDataOptions;
|
||||||
|
|
||||||
|
private static readonly SemaphoreSlim WriteSemaphore = new(1, 1);
|
||||||
|
private static readonly Guid SaveTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
|
public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_annConfig = annConfig.Value;
|
_annConfig = annConfig.Value;
|
||||||
|
|
||||||
_memoryConnection = new SQLiteConnection(MemoryConnStr);
|
_memoryConnection = new SQLiteConnection(MemoryConnStr);
|
||||||
@@ -43,7 +47,7 @@ public class DbFactory : IDbFactory
|
|||||||
.UseDataProvider(SQLiteTools.GetDataProvider())
|
.UseDataProvider(SQLiteTools.GetDataProvider())
|
||||||
.UseConnection(_memoryConnection)
|
.UseConnection(_memoryConnection)
|
||||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
|
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
|
||||||
;//.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
||||||
|
|
||||||
|
|
||||||
_fileConnection = new SQLiteConnection(FileConnStr);
|
_fileConnection = new SQLiteConnection(FileConnStr);
|
||||||
@@ -53,32 +57,31 @@ public class DbFactory : IDbFactory
|
|||||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||||
|
|
||||||
if (!File.Exists(_annConfig.AnnotationsDbFile))
|
if (!File.Exists(_annConfig.AnnotationsDbFile))
|
||||||
CreateDb();
|
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
|
||||||
|
RecreateTables();
|
||||||
|
|
||||||
_fileConnection.Open();
|
_fileConnection.Open();
|
||||||
|
using var db = new AnnotationsDb(_fileDataOptions);
|
||||||
|
var entityTypes = typeof(AnnotationsDb)
|
||||||
|
.GetProperties()
|
||||||
|
.Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ITable<>))
|
||||||
|
.Select(p => p.PropertyType.GetGenericArguments()[0])
|
||||||
|
.ToArray();
|
||||||
|
SchemaMigrator.EnsureSchemaUpdated(db, entityTypes);
|
||||||
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
|
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateDb()
|
private void RecreateTables()
|
||||||
{
|
{
|
||||||
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
|
|
||||||
using var db = new AnnotationsDb(_fileDataOptions);
|
using var db = new AnnotationsDb(_fileDataOptions);
|
||||||
db.CreateTable<Annotation>();
|
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
|
||||||
db.CreateTable<AnnotationName>();
|
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
|
||||||
db.CreateTable<Detection>();
|
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
|
||||||
db.CreateTable<QueueOffset>();
|
db.CreateTable<Annotation>();
|
||||||
db.QueueOffsets.BulkCopy(new List<QueueOffset>
|
if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
|
||||||
{
|
db.CreateTable<Detection>();
|
||||||
new()
|
if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
|
||||||
{
|
db.CreateTable<AnnotationQueueRecord>();
|
||||||
Offset = 0,
|
|
||||||
QueueName = Constants.MQ_ANNOTATIONS_QUEUE
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Offset = 0,
|
|
||||||
QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
@@ -87,64 +90,62 @@ public class DbFactory : IDbFactory
|
|||||||
return await func(db);
|
return await func(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Run(Func<AnnotationsDb, Task> func)
|
public async Task RunWrite(Func<AnnotationsDb, Task> func)
|
||||||
{
|
{
|
||||||
await using var db = new AnnotationsDb(_memoryDataOptions);
|
await WriteSemaphore.WaitAsync();
|
||||||
await func(db);
|
try
|
||||||
|
{
|
||||||
|
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||||
|
await func(db);
|
||||||
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
WriteSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveToDisk()
|
public async Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
{
|
{
|
||||||
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
await WriteSemaphore.WaitAsync();
|
||||||
}
|
try
|
||||||
|
{
|
||||||
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
|
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||||
{
|
var result = await func(db);
|
||||||
var names = annotations.Select(x => x.Name).ToList();
|
ThrottleExt.Throttle(async () =>
|
||||||
await DeleteAnnotations(names, cancellationToken);
|
{
|
||||||
|
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
WriteSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
|
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await Run(async db =>
|
await RunWrite(async db =>
|
||||||
{
|
{
|
||||||
var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken);
|
var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken);
|
||||||
var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
|
var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
|
||||||
Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations");
|
_logger.LogInformation($"Deleted {detDeleted} detections, {annDeleted} annotations");
|
||||||
});
|
});
|
||||||
SaveToDisk();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AnnotationsDbSchemaHolder
|
|
||||||
{
|
|
||||||
public static readonly MappingSchema MappingSchema;
|
|
||||||
|
|
||||||
static AnnotationsDbSchemaHolder()
|
|
||||||
{
|
|
||||||
MappingSchema = new MappingSchema();
|
|
||||||
var builder = new FluentMappingBuilder(MappingSchema);
|
|
||||||
|
|
||||||
var annotationBuilder = builder.Entity<Annotation>();
|
|
||||||
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
|
||||||
.HasPrimaryKey(x => x.Name)
|
|
||||||
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
|
|
||||||
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
|
|
||||||
|
|
||||||
annotationBuilder
|
|
||||||
.Ignore(x => x.Milliseconds)
|
|
||||||
.Ignore(x => x.Classes)
|
|
||||||
.Ignore(x => x.Classes)
|
|
||||||
.Ignore(x => x.ImagePath)
|
|
||||||
.Ignore(x => x.LabelPath)
|
|
||||||
.Ignore(x => x.ThumbPath);
|
|
||||||
|
|
||||||
builder.Entity<Detection>()
|
|
||||||
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
|
||||||
|
|
||||||
builder.Entity<AnnotationName>()
|
|
||||||
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME);
|
|
||||||
|
|
||||||
builder.Build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Azaion.Common.DTO;
|
||||||
|
using MessagePack;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class Detection : YoloLabel
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
|
||||||
|
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "dn")] [Key("dn")] public string Description { get; set; } = null!;
|
||||||
|
[JsonProperty(PropertyName = "af")] [Key("af")] public AffiliationEnum Affiliation { get; set; }
|
||||||
|
|
||||||
|
//For db & serialization
|
||||||
|
public Detection(){}
|
||||||
|
|
||||||
|
public Detection(string annotationName, YoloLabel label, string description = "", double confidence = 1)
|
||||||
|
{
|
||||||
|
AnnotationName = annotationName;
|
||||||
|
Description = description;
|
||||||
|
ClassNumber = label.ClassNumber;
|
||||||
|
CenterX = label.CenterX;
|
||||||
|
CenterY = label.CenterY;
|
||||||
|
Height = label.Height;
|
||||||
|
Width = label.Width;
|
||||||
|
Confidence = confidence;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class MediaFile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
public string MediaUrl { get; set; } = null!;
|
||||||
|
public DateTime? LastProcessedDate { get; set; }
|
||||||
|
public MediaStatus Status { get; set; } = MediaStatus.New;
|
||||||
|
public int? RecognisedObjects { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MediaStatus
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
New,
|
||||||
|
AIProcessing,
|
||||||
|
AIProcessed,
|
||||||
|
ManualConfirmed,
|
||||||
|
Error
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Azaion.Common.Database;
|
|
||||||
|
|
||||||
public class QueueOffset
|
|
||||||
{
|
|
||||||
public string QueueName { get; set; } = null!;
|
|
||||||
public ulong Offset { get; set; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Data;
|
||||||
|
using LinqToDB.Data;
|
||||||
|
using LinqToDB.Mapping;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public static class SchemaMigrator
|
||||||
|
{
|
||||||
|
public static void EnsureSchemaUpdated(DataConnection dbConnection, params Type[] entityTypes)
|
||||||
|
{
|
||||||
|
var connection = dbConnection.Connection;
|
||||||
|
var mappingSchema = dbConnection.MappingSchema;
|
||||||
|
|
||||||
|
if (connection.State == ConnectionState.Closed)
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var type in entityTypes)
|
||||||
|
{
|
||||||
|
var entityDescriptor = mappingSchema.GetEntityDescriptor(type);
|
||||||
|
var tableName = entityDescriptor.Name.Name;
|
||||||
|
var existingColumns = GetTableColumns(connection, tableName);
|
||||||
|
if (existingColumns.Count == 0) // table does not exist
|
||||||
|
{
|
||||||
|
var columnDefinitions = entityDescriptor.Columns.Select(GetColumnDefinition);
|
||||||
|
dbConnection.Execute($"CREATE TABLE {tableName} ({string.Join(", ", columnDefinitions)})");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var column in entityDescriptor.Columns)
|
||||||
|
{
|
||||||
|
if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var columnDefinition = GetColumnDefinition(column);
|
||||||
|
dbConnection.Execute($"ALTER TABLE {tableName} ADD COLUMN {columnDefinition}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<string> GetTableColumns(IDbConnection connection, string tableName)
|
||||||
|
{
|
||||||
|
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = $"PRAGMA table_info({tableName})";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
columns.Add(reader.GetString(1)); // "name" is in the second column
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetColumnDefinition(ColumnDescriptor column)
|
||||||
|
{
|
||||||
|
var type = column.MemberType;
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
|
||||||
|
var sqliteType = GetSqliteType(underlyingType);
|
||||||
|
var defaultClause = GetSqlDefaultValue(type, underlyingType);
|
||||||
|
|
||||||
|
return $"\"{column.ColumnName}\" {sqliteType} {defaultClause}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSqliteType(Type type) =>
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
_ when type == typeof(int)
|
||||||
|
|| type == typeof(long)
|
||||||
|
|| type == typeof(bool)
|
||||||
|
|| type.IsEnum
|
||||||
|
=> "INTEGER",
|
||||||
|
|
||||||
|
_ when type == typeof(double)
|
||||||
|
|| type == typeof(float)
|
||||||
|
|| type == typeof(decimal)
|
||||||
|
=> "REAL",
|
||||||
|
|
||||||
|
_ when type == typeof(byte[])
|
||||||
|
=> "BLOB",
|
||||||
|
|
||||||
|
_ => "TEXT"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetSqlDefaultValue(Type originalType, Type underlyingType)
|
||||||
|
{
|
||||||
|
var isNullable = originalType.IsClass || Nullable.GetUnderlyingType(originalType) != null;
|
||||||
|
if (isNullable)
|
||||||
|
return "NULL";
|
||||||
|
|
||||||
|
var defaultValue = Activator.CreateInstance(underlyingType);
|
||||||
|
|
||||||
|
if (underlyingType == typeof(bool))
|
||||||
|
return $"NOT NULL DEFAULT {(Convert.ToBoolean(defaultValue) ? 1 : 0)}";
|
||||||
|
|
||||||
|
if (underlyingType.IsEnum)
|
||||||
|
return $"NOT NULL DEFAULT {(int)(defaultValue ?? 0)}";
|
||||||
|
|
||||||
|
if (underlyingType.IsValueType && defaultValue is IFormattable f)
|
||||||
|
return $"NOT NULL DEFAULT {f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)}";
|
||||||
|
|
||||||
|
return $"NOT NULL DEFAULT '{defaultValue}'";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,13 @@ using MediatR;
|
|||||||
|
|
||||||
namespace Azaion.Common.Events;
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
|
public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
|
||||||
{
|
{
|
||||||
public List<Annotation> Annotations { get; set; } = annotations;
|
public List<string> AnnotationNames { get; set; } = annotationNames;
|
||||||
|
public bool FromQueue { get; set; } = fromQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnnotationAddedEvent(Annotation annotation) : INotification
|
||||||
|
{
|
||||||
|
public Annotation Annotation { get; set; } = annotation;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
|
public class LoadErrorEvent(string error) : INotification
|
||||||
|
{
|
||||||
|
public string Error { get; set; } = error;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
|
public class SetStatusTextEvent(string text, bool isError = false) : INotification
|
||||||
|
{
|
||||||
|
public string Text { get; set; } = text;
|
||||||
|
public bool IsError { get; set; } = isError;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Azaion.CommonSecurity.Exceptions;
|
||||||
|
|
||||||
|
public class BusinessException(string message) : Exception(message);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
namespace Azaion.Common.Extensions;
|
namespace Azaion.Common.Extensions;
|
||||||
@@ -22,4 +23,17 @@ public static class BitmapExtensions
|
|||||||
image.Freeze();
|
image.Freeze();
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Color CreateTransparent(this Color color, byte transparency) =>
|
||||||
|
Color.FromArgb(transparency, color.R, color.G, color.B);
|
||||||
|
|
||||||
|
public static async Task SaveImage(this BitmapSource bitmap, string path, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await using var stream = new FileStream(path, FileMode.Create);
|
||||||
|
var encoder = new JpegBitmapEncoder();
|
||||||
|
|
||||||
|
encoder.Frames.Add(BitmapFrame.Create(bitmap));
|
||||||
|
encoder.Save(stream);
|
||||||
|
await stream.FlushAsync(ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class CancellationTokenExtensions
|
||||||
|
{
|
||||||
|
public static void WaitForCancel(this CancellationToken token, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task.Delay(timeout, token).Wait(token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
//Don't need to catch exception, need only return from the waiting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task AsTask(this CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!cancellationToken.CanBeCanceled)
|
||||||
|
return new TaskCompletionSource<bool>().Task;
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return Task.FromCanceled(cancellationToken);
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
|
||||||
|
tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
|
||||||
|
return tcs.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class EnumExtensions
|
||||||
|
{
|
||||||
|
public static T GetValueOrDefault<T>(this string value, T defaultValue) where T : struct
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return defaultValue;
|
||||||
|
|
||||||
|
return Enum.TryParse(value, true, out T result) ? result : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
public static class GeoUtils
|
public static class GeoUtils
|
||||||
{
|
{
|
||||||
@@ -13,26 +15,71 @@ public static class GeoUtils
|
|||||||
return (xTile, yTile);
|
return (xTile, yTile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom)
|
public static double ToRadians(double degrees) => degrees * Math.PI / 180.0;
|
||||||
|
public static double ToDegrees(double radians) => radians * 180.0 / Math.PI;
|
||||||
|
|
||||||
|
public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2)
|
||||||
|
{
|
||||||
|
var lat1Rad = ToRadians(p1.Lat);
|
||||||
|
var lat2Rad = ToRadians(p2.Lat);
|
||||||
|
var dLon = ToRadians(p2.Lon - p1.Lon);
|
||||||
|
var dLat = ToRadians(p2.Lat - p1.Lat);
|
||||||
|
|
||||||
|
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
|
||||||
|
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
|
||||||
|
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
|
||||||
|
var c = 2 * Math.Asin(Math.Sqrt(a));
|
||||||
|
var distance = EARTH_RADIUS * c;
|
||||||
|
|
||||||
|
var y = Math.Sin(dLon) * Math.Cos(lat2Rad);
|
||||||
|
var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
|
||||||
|
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon);
|
||||||
|
var azimuthRadians = Math.Atan2(y, x);
|
||||||
|
var azimuth = (ToDegrees(azimuthRadians) + 360) % 360;
|
||||||
|
|
||||||
|
return new Direction
|
||||||
|
{
|
||||||
|
Distance = distance,
|
||||||
|
Azimuth = azimuth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction)
|
||||||
|
{
|
||||||
|
var angularDistance = direction.Distance / EARTH_RADIUS;
|
||||||
|
var azimuthRadians = ToRadians(direction.Azimuth);
|
||||||
|
var startLatRad = ToRadians(startPoint.Lat);
|
||||||
|
var startLonRad = ToRadians(startPoint.Lon);
|
||||||
|
|
||||||
|
var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) +
|
||||||
|
Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians));
|
||||||
|
|
||||||
|
var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad),
|
||||||
|
Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad));
|
||||||
|
|
||||||
|
return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GeoPoint TileToWorldPos(int x, int y, int zoom)
|
||||||
{
|
{
|
||||||
var n = Math.Pow(2.0, zoom);
|
var n = Math.Pow(2.0, zoom);
|
||||||
var lonDeg = x / n * 360.0 - 180.0;
|
var lonDeg = x / n * 360.0 - 180.0;
|
||||||
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
|
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
|
||||||
var latDeg = latRad * 180.0 / Math.PI;
|
var latDeg = latRad * 180.0 / Math.PI;
|
||||||
return (latDeg, lonDeg);
|
return new GeoPoint(latDeg, lonDeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
|
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
|
||||||
{
|
{
|
||||||
var latRad = centerLat * Math.PI / 180.0;
|
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
|
||||||
|
|
||||||
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
||||||
var minLat = Math.Max(centerLat - latDiff, -90.0);
|
var minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0);
|
||||||
var maxLat = Math.Min(centerLat + latDiff, 90.0);
|
var maxLat = Math.Min(centerGeoPoint.Lat + latDiff, 90.0);
|
||||||
|
|
||||||
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
|
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
|
||||||
var minLon = Math.Max(centerLon - lonDiff, -180.0);
|
var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
|
||||||
var maxLon = Math.Min(centerLon + lonDiff, 180.0);
|
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
|
||||||
|
|
||||||
return (minLat, maxLat, minLon, maxLon);
|
return (minLat, maxLat, minLon, maxLon);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class QueryableExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Where true predicate only if result of condition is true.
|
||||||
|
/// If false predicate provided, uses it in case of false result
|
||||||
|
/// Useful for filters, when filters should be applied only when it was set (not NULL)
|
||||||
|
/// </summary>
|
||||||
|
public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> query, bool? condition,
|
||||||
|
Expression<Func<TSource, bool>> truePredicate,
|
||||||
|
Expression<Func<TSource, bool>>? falsePredicate = null)
|
||||||
|
{
|
||||||
|
if (!condition.HasValue)
|
||||||
|
return query;
|
||||||
|
|
||||||
|
if (condition.Value)
|
||||||
|
return query.Where(truePredicate);
|
||||||
|
|
||||||
|
return falsePredicate != null
|
||||||
|
? query.Where(falsePredicate)
|
||||||
|
: query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds Where true predicate only if result of condition is true.
|
||||||
|
/// If false predicate provided, uses it in case of false result
|
||||||
|
/// Useful for filters, when filters should be applied only when it was set (not NULL)
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TSource> WhereIf<TSource>(this IEnumerable<TSource> query, bool? condition,
|
||||||
|
Func<TSource, bool> truePredicate,
|
||||||
|
Func<TSource, bool>? falsePredicate = null)
|
||||||
|
{
|
||||||
|
if (!condition.HasValue)
|
||||||
|
return query;
|
||||||
|
|
||||||
|
if (condition.Value)
|
||||||
|
return query.Where(truePredicate);
|
||||||
|
|
||||||
|
return falsePredicate != null
|
||||||
|
? query.Where(falsePredicate)
|
||||||
|
: query;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Polly;
|
||||||
|
|
||||||
|
public static class ResilienceExt
|
||||||
|
{
|
||||||
|
public static void WithRetry(this Action operation, int retryCount = 3, int delayMs = 150) =>
|
||||||
|
Policy.Handle<Exception>()
|
||||||
|
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
|
||||||
|
.Execute(operation);
|
||||||
|
|
||||||
|
public static TResult WithRetry<TResult>(this Func<TResult> operation, int retryCount = 3, int delayMs = 150) =>
|
||||||
|
Policy.Handle<Exception>()
|
||||||
|
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
|
||||||
|
.Execute(operation);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class SizeExtensions
|
||||||
|
{
|
||||||
|
public static bool FitSizeForAI(this Size size) =>
|
||||||
|
// Allow to be up to FullHD to save as 1280*1280
|
||||||
|
size.Width <= Constants.AI_TILE_SIZE_DEFAULT * 1.5 && size.Height <= Constants.AI_TILE_SIZE_DEFAULT * 1.5;
|
||||||
|
}
|
||||||
@@ -54,7 +54,6 @@ public static class ThrottleExt
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await Task.Delay(interval);
|
await Task.Delay(interval);
|
||||||
|
|
||||||
lock (state.StateLock)
|
lock (state.StateLock)
|
||||||
{
|
{
|
||||||
if (state.CallScheduledDuringCooldown)
|
if (state.CallScheduledDuringCooldown)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Azaion.Common;
|
||||||
|
|
||||||
|
public class Security
|
||||||
|
{
|
||||||
|
private static string GenDefaultKey()
|
||||||
|
{
|
||||||
|
var date = DateTime.UtcNow;
|
||||||
|
return $"sAzaion_default_dfvkjhg_{date:yyyy}-{date:MM}_{date:dd}_{date:HH}_key";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Encrypt<T>(T model, string? key = null) where T : class
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(model);
|
||||||
|
var inputBytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
|
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
|
||||||
|
var iv = RandomNumberGenerator.GetBytes(16);
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = keyBytes;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CFB;
|
||||||
|
aes.Padding = PaddingMode.ISO10126;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var ciphertext = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
|
||||||
|
|
||||||
|
var result = new byte[iv.Length + ciphertext.Length];
|
||||||
|
iv.CopyTo(result, 0);
|
||||||
|
ciphertext.CopyTo(result, iv.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Decrypt<T>(string encryptedData, string? key = null) where T : class
|
||||||
|
{
|
||||||
|
var ciphertextWithIv = Convert.FromBase64String(encryptedData);
|
||||||
|
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
|
||||||
|
|
||||||
|
var iv = ciphertextWithIv[..16];
|
||||||
|
var ciphertext = ciphertextWithIv[16..];
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = keyBytes;
|
||||||
|
aes.IV = iv;
|
||||||
|
aes.Mode = CipherMode.CFB;
|
||||||
|
aes.Padding = PaddingMode.ISO10126;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
var plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
|
||||||
|
|
||||||
|
var json = Encoding.UTF8.GetString(plaintext);
|
||||||
|
return JsonConvert.DeserializeObject<T>(json)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Drawing.Imaging;
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
@@ -7,12 +8,11 @@ using Azaion.Common.DTO.Config;
|
|||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using Azaion.Common.Events;
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Azaion.CommonSecurity.Services;
|
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RabbitMQ.Stream.Client;
|
using RabbitMQ.Stream.Client;
|
||||||
@@ -20,17 +20,23 @@ using RabbitMQ.Stream.Client.Reliable;
|
|||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
// SHOULD BE ONLY ONE INSTANCE OF AnnotationService. Do not add ANY NotificationHandler to it!
|
||||||
|
// Queue consumer should be created only once.
|
||||||
|
public class AnnotationService : IAnnotationService
|
||||||
{
|
{
|
||||||
private readonly IDbFactory _dbFactory;
|
private readonly IDbFactory _dbFactory;
|
||||||
private readonly FailsafeAnnotationsProducer _producer;
|
private readonly FailsafeAnnotationsProducer _producer;
|
||||||
private readonly IGalleryService _galleryService;
|
private readonly IGalleryService _galleryService;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IAzaionApi _api;
|
private readonly IAzaionApi _api;
|
||||||
|
private readonly ILogger<AnnotationService> _logger;
|
||||||
private readonly QueueConfig _queueConfig;
|
private readonly QueueConfig _queueConfig;
|
||||||
private Consumer _consumer = null!;
|
private Consumer _consumer = null!;
|
||||||
private readonly UIConfig _uiConfig;
|
private readonly UIConfig _uiConfig;
|
||||||
private static readonly Guid SaveTaskId = Guid.NewGuid();
|
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
|
||||||
|
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
|
|
||||||
public AnnotationService(
|
public AnnotationService(
|
||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
@@ -39,20 +45,22 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
IOptions<UIConfig> uiConfig,
|
IOptions<UIConfig> uiConfig,
|
||||||
IGalleryService galleryService,
|
IGalleryService galleryService,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IAzaionApi api)
|
IAzaionApi api,
|
||||||
|
ILogger<AnnotationService> logger)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_producer = producer;
|
_producer = producer;
|
||||||
_galleryService = galleryService;
|
_galleryService = galleryService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_api = api;
|
_api = api;
|
||||||
|
_logger = logger;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
_uiConfig = uiConfig.Value;
|
_uiConfig = uiConfig.Value;
|
||||||
|
|
||||||
Task.Run(async () => await Init()).Wait();
|
Task.Run(async () => await InitQueueConsumer()).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init(CancellationToken cancellationToken = default)
|
private async Task InitQueueConsumer(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!_api.CurrentUser.Role.IsValidator())
|
if (!_api.CurrentUser.Role.IsValidator())
|
||||||
return;
|
return;
|
||||||
@@ -69,32 +77,59 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
||||||
{
|
{
|
||||||
Reference = _api.CurrentUser.Email,
|
Reference = _api.CurrentUser.Email,
|
||||||
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1),
|
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
|
||||||
MessageHandler = async (_, _, context, message) =>
|
MessageHandler = async (_, _, context, message) =>
|
||||||
{
|
{
|
||||||
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
|
await _messageProcessingSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
offsets.AnnotationsOffset = context.Offset;
|
|
||||||
ThrottleExt.Throttle(() =>
|
|
||||||
{
|
{
|
||||||
_api.UpdateOffsets(offsets);
|
var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
|
||||||
return Task.CompletedTask;
|
if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
|
||||||
}, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
|
return;
|
||||||
|
|
||||||
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
|
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
|
||||||
return;
|
{
|
||||||
|
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
|
||||||
await SaveAnnotationInner(
|
{
|
||||||
msg.CreatedDate,
|
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
|
||||||
msg.OriginalMediaName,
|
await SaveAnnotationInner(
|
||||||
msg.Time,
|
msg.CreatedDate,
|
||||||
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
|
msg.OriginalMediaName,
|
||||||
msg.Source,
|
msg.Name,
|
||||||
new MemoryStream(msg.Image),
|
msg.Time,
|
||||||
msg.CreatedRole,
|
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
|
||||||
msg.CreatedEmail,
|
msg.Source,
|
||||||
fromQueue: true,
|
msg.Image == null ? null : new MemoryStream(msg.Image),
|
||||||
token: cancellationToken);
|
msg.Role,
|
||||||
|
msg.Email,
|
||||||
|
context.Offset,
|
||||||
|
token: token);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var msg = MessagePackSerializer.Deserialize<AnnotationBulkMessage>(message.Data.Contents);
|
||||||
|
if (annotationStatus == AnnotationStatus.Validated)
|
||||||
|
await ValidateAnnotations(msg.AnnotationNames.ToList(), true, token);
|
||||||
|
if (annotationStatus == AnnotationStatus.Deleted)
|
||||||
|
await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message
|
||||||
|
ThrottleExt.Throttle(() =>
|
||||||
|
{
|
||||||
|
_api.UpdateOffsets(offsets);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_messageProcessingSemaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -103,44 +138,50 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
|
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
|
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
|
||||||
return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(),
|
return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Name, a.Time, a.Detections.ToList(),
|
||||||
SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
|
SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Manual
|
//Manual
|
||||||
public async Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
|
public async Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
|
||||||
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream,
|
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, annotationName, time, detections, SourceEnum.Manual, stream,
|
||||||
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
|
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
|
||||||
|
|
||||||
// Manual save from Validators -> Validated -> stream: azaion-annotations-confirm
|
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, string annotationName, TimeSpan time,
|
||||||
// AI, Manual save from Operators -> Created -> stream: azaion-annotations
|
List<Detection> detections, SourceEnum source, Stream? stream,
|
||||||
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List<Detection> detections, SourceEnum source, Stream? stream,
|
|
||||||
RoleEnum userRole,
|
RoleEnum userRole,
|
||||||
string createdEmail,
|
string createdEmail,
|
||||||
bool fromQueue = false,
|
ulong? offset = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
AnnotationStatus status;
|
var status = AnnotationStatus.Created;
|
||||||
var fName = originalMediaName.ToTimeName(time);
|
var annotation = await _dbFactory.RunWrite(async db =>
|
||||||
var annotation = await _dbFactory.Run(async db =>
|
|
||||||
{
|
{
|
||||||
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
var ann = await db.Annotations
|
||||||
status = userRole.IsValidator() && source == SourceEnum.Manual
|
.LoadWith(x => x.Detections)
|
||||||
? AnnotationStatus.Validated
|
.FirstOrDefaultAsync(x => x.Name == annotationName, token: token);
|
||||||
: AnnotationStatus.Created;
|
|
||||||
|
|
||||||
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
|
await db.Detections.DeleteAsync(x => x.AnnotationName == annotationName, token: token);
|
||||||
|
|
||||||
if (ann != null)
|
if (ann != null) //Annotation is already exists
|
||||||
{
|
{
|
||||||
await db.Annotations
|
status = AnnotationStatus.Edited;
|
||||||
.Where(x => x.Name == fName)
|
|
||||||
.Set(x => x.Source, source)
|
var annotationUpdatable = db.Annotations
|
||||||
|
.Where(x => x.Name == annotationName)
|
||||||
|
.Set(x => x.Source, source);
|
||||||
|
|
||||||
|
if (userRole.IsValidator() && source == SourceEnum.Manual)
|
||||||
|
{
|
||||||
|
annotationUpdatable = annotationUpdatable
|
||||||
|
.Set(x => x.ValidateDate, createdDate)
|
||||||
|
.Set(x => x.ValidateEmail, createdEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
await annotationUpdatable
|
||||||
.Set(x => x.AnnotationStatus, status)
|
.Set(x => x.AnnotationStatus, status)
|
||||||
.Set(x => x.CreatedDate, createdDate)
|
|
||||||
.Set(x => x.CreatedEmail, createdEmail)
|
|
||||||
.Set(x => x.CreatedRole, userRole)
|
|
||||||
.UpdateAsync(token: token);
|
.UpdateAsync(token: token);
|
||||||
|
|
||||||
ann.Detections = detections;
|
ann.Detections = detections;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -148,7 +189,7 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
ann = new Annotation
|
ann = new Annotation
|
||||||
{
|
{
|
||||||
CreatedDate = createdDate,
|
CreatedDate = createdDate,
|
||||||
Name = fName,
|
Name = annotationName,
|
||||||
OriginalMediaName = originalMediaName,
|
OriginalMediaName = originalMediaName,
|
||||||
Time = time,
|
Time = time,
|
||||||
ImageExtension = Constants.JPG_EXT,
|
ImageExtension = Constants.JPG_EXT,
|
||||||
@@ -164,36 +205,50 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
return ann;
|
return ann;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stream != null)
|
//Save image should be done in 1 thread only
|
||||||
|
await _imageAccessSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var img = System.Drawing.Image.FromStream(stream);
|
Image image = null!;
|
||||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
if (stream != null)
|
||||||
|
{
|
||||||
|
image = Image.FromStream(stream);
|
||||||
|
if (File.Exists(annotation.ImagePath))
|
||||||
|
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
|
||||||
|
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
||||||
|
|
||||||
|
await _galleryService.CreateThumbnail(annotation, image, token);
|
||||||
|
if (_uiConfig.GenerateAnnotatedImage)
|
||||||
|
await _galleryService.CreateAnnotatedImage(annotation, image, token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_imageAccessSemaphore.Release();
|
||||||
}
|
}
|
||||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
|
||||||
|
|
||||||
await _galleryService.CreateThumbnail(annotation, token);
|
|
||||||
if (_uiConfig.GenerateAnnotatedImage)
|
|
||||||
await _galleryService.CreateAnnotatedImage(annotation, token);
|
|
||||||
|
|
||||||
if (!fromQueue && !_uiConfig.SilentDetection) //Send to queue only if we're not getting from queue already
|
|
||||||
await _producer.SendToInnerQueue(annotation, token);
|
|
||||||
|
|
||||||
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
||||||
ThrottleExt.Throttle(async () =>
|
|
||||||
{
|
if (!offset.HasValue) //Send to queue only if we're not getting from queue already
|
||||||
_dbFactory.SaveToDisk();
|
await _producer.SendToInnerQueue([annotation.Name], status, token);
|
||||||
await Task.CompletedTask;
|
|
||||||
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
|
||||||
return annotation;
|
return annotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ValidateAnnotations(List<Annotation> annotations, CancellationToken token = default)
|
public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!_api.CurrentUser.Role.IsValidator())
|
if (!_api.CurrentUser.Role.IsValidator())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var annNames = annotations.Select(x => x.Name).ToHashSet();
|
var annNames = annotationNames.ToHashSet();
|
||||||
await _dbFactory.Run(async db =>
|
await _dbFactory.RunWrite(async db =>
|
||||||
{
|
{
|
||||||
await db.Annotations
|
await db.Annotations
|
||||||
.Where(x => annNames.Contains(x.Name))
|
.Where(x => annNames.Contains(x.Name))
|
||||||
@@ -202,21 +257,14 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
|
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
|
||||||
.UpdateAsync(token: token);
|
.UpdateAsync(token: token);
|
||||||
});
|
});
|
||||||
ThrottleExt.Throttle(async () =>
|
if (!fromQueue)
|
||||||
{
|
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
|
||||||
_dbFactory.SaveToDisk();
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
|
public interface IAnnotationService
|
||||||
{
|
{
|
||||||
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken);
|
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
|
||||||
foreach (var annotation in notification.Annotations)
|
Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
|
||||||
{
|
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
|
||||||
File.Delete(annotation.ImagePath);
|
|
||||||
File.Delete(annotation.LabelPath);
|
|
||||||
File.Delete(annotation.ThumbPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+12
-8
@@ -1,10 +1,14 @@
|
|||||||
using System.Net;
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
using Newtonsoft.Json;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public interface IAzaionApi
|
public interface IAzaionApi
|
||||||
{
|
{
|
||||||
@@ -14,7 +18,7 @@ public interface IAzaionApi
|
|||||||
//Stream GetResource(string filename, string folder);
|
//Stream GetResource(string filename, string folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
|
public class AzaionApi(ILogger logger, HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
|
||||||
{
|
{
|
||||||
private string _jwtToken = null!;
|
private string _jwtToken = null!;
|
||||||
const string APP_JSON = "application/json";
|
const string APP_JSON = "application/json";
|
||||||
@@ -24,8 +28,8 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
|
var user = cache.GetFromCache(Constants.CURRENT_USER_CACHE_KEY,
|
||||||
() => Get<User>("currentUser"));
|
() => Get<User>("users/current"));
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new Exception("Can't get current user");
|
throw new Exception("Can't get current user");
|
||||||
return user;
|
return user;
|
||||||
@@ -114,7 +118,7 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Console.WriteLine(e);
|
logger.Error(e, e.Message);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using LazyCache;
|
using LazyCache;
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public interface ICache
|
public interface ICache
|
||||||
{
|
{
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using RabbitMQ.Stream.Client;
|
using RabbitMQ.Stream.Client;
|
||||||
|
using RabbitMQ.Stream.Client.AMQP;
|
||||||
using RabbitMQ.Stream.Client.Reliable;
|
using RabbitMQ.Stream.Client.Reliable;
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.Common.Services;
|
||||||
@@ -17,17 +20,24 @@ public class FailsafeAnnotationsProducer
|
|||||||
{
|
{
|
||||||
private readonly ILogger<FailsafeAnnotationsProducer> _logger;
|
private readonly ILogger<FailsafeAnnotationsProducer> _logger;
|
||||||
private readonly IDbFactory _dbFactory;
|
private readonly IDbFactory _dbFactory;
|
||||||
|
private readonly IAzaionApi _azaionApi;
|
||||||
private readonly QueueConfig _queueConfig;
|
private readonly QueueConfig _queueConfig;
|
||||||
|
private readonly UIConfig _uiConfig;
|
||||||
|
|
||||||
private Producer _annotationProducer = null!;
|
private Producer _annotationProducer = null!;
|
||||||
private Producer _annotationConfirmProducer = null!;
|
|
||||||
|
|
||||||
|
|
||||||
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig)
|
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger,
|
||||||
|
IDbFactory dbFactory,
|
||||||
|
IOptions<QueueConfig> queueConfig,
|
||||||
|
IOptions<UIConfig> uiConfig,
|
||||||
|
IAzaionApi azaionApi)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
|
_azaionApi = azaionApi;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
|
_uiConfig = uiConfig.Value;
|
||||||
Task.Run(async () => await ProcessQueue());
|
Task.Run(async () => await ProcessQueue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,107 +51,112 @@ public class FailsafeAnnotationsProducer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init(CancellationToken cancellationToken = default)
|
private async Task ProcessQueue(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
|
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
|
||||||
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE));
|
while (!ct.IsCancellationRequested)
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessQueue(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await Init(cancellationToken);
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
{
|
||||||
var messages = await GetFromInnerQueue(cancellationToken);
|
var sent = false;
|
||||||
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
while (!sent || !ct.IsCancellationRequested) //Waiting for send
|
||||||
{
|
|
||||||
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();
|
|
||||||
if (createdMessages.Any())
|
|
||||||
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
|
||||||
|
|
||||||
var validatedMessages = messagesChunk
|
|
||||||
.Where(x => x.Status == AnnotationStatus.Validated)
|
|
||||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
|
||||||
.ToList();
|
|
||||||
if (validatedMessages.Any())
|
|
||||||
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;
|
|
||||||
_dbFactory.SaveToDisk();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, e.Message);
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
|
||||||
}
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return await _dbFactory.Run(async db =>
|
|
||||||
{
|
|
||||||
var annotations = await db.AnnotationsQueue.Join(
|
|
||||||
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a)
|
|
||||||
.ToListAsync(token: cancellationToken);
|
|
||||||
|
|
||||||
var messages = new List<AnnotationCreatedMessage>();
|
|
||||||
var badImages = new List<string>();
|
|
||||||
foreach (var annotation in annotations)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
var (records, annotationsDict) = await _dbFactory.Run(async db =>
|
||||||
var annCreateMessage = new AnnotationCreatedMessage
|
|
||||||
{
|
{
|
||||||
Name = annotation.Name,
|
var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
|
||||||
OriginalMediaName = annotation.OriginalMediaName,
|
var editedCreatedNames = records
|
||||||
Time = annotation.Time,
|
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
|
||||||
CreatedRole = annotation.CreatedRole,
|
.Select(x => x.AnnotationNames.FirstOrDefault())
|
||||||
CreatedEmail = annotation.CreatedEmail,
|
.ToList();
|
||||||
CreatedDate = annotation.CreatedDate,
|
|
||||||
Status = annotation.AnnotationStatus,
|
|
||||||
|
|
||||||
ImageExtension = annotation.ImageExtension,
|
var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
|
||||||
Image = image,
|
.Where(x => editedCreatedNames.Contains(x.Name))
|
||||||
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
.ToDictionaryAsync(a => a.Name, token: ct);
|
||||||
Source = annotation.Source,
|
return (records, annotationsDict);
|
||||||
};
|
});
|
||||||
messages.Add(annCreateMessage);
|
|
||||||
|
var messages = new List<Message>();
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
var appProperties = new ApplicationProperties
|
||||||
|
{
|
||||||
|
{ nameof(AnnotationStatus), record.Operation.ToString() },
|
||||||
|
{ nameof(User.Email), _azaionApi.CurrentUser.Email }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted))
|
||||||
|
{
|
||||||
|
var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage
|
||||||
|
{
|
||||||
|
AnnotationNames = record.AnnotationNames.ToArray(),
|
||||||
|
AnnotationStatus = record.Operation,
|
||||||
|
Email = _azaionApi.CurrentUser.Email,
|
||||||
|
CreatedDate = record.DateTime
|
||||||
|
})) { ApplicationProperties = appProperties };
|
||||||
|
|
||||||
|
messages.Add(message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault());
|
||||||
|
if (annotation == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var image = record.Operation == AnnotationStatus.Created
|
||||||
|
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var annMessage = new AnnotationMessage
|
||||||
|
{
|
||||||
|
Name = annotation.Name,
|
||||||
|
OriginalMediaName = annotation.OriginalMediaName,
|
||||||
|
Time = annotation.Time,
|
||||||
|
Role = annotation.CreatedRole,
|
||||||
|
Email = annotation.CreatedEmail,
|
||||||
|
CreatedDate = annotation.CreatedDate,
|
||||||
|
Status = annotation.AnnotationStatus,
|
||||||
|
|
||||||
|
ImageExtension = annotation.ImageExtension,
|
||||||
|
Image = image,
|
||||||
|
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
||||||
|
Source = annotation.Source,
|
||||||
|
};
|
||||||
|
var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties };
|
||||||
|
|
||||||
|
messages.Add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.Any())
|
||||||
|
{
|
||||||
|
await _annotationProducer.Send(messages, CompressionType.Gzip);
|
||||||
|
var ids = records.Select(x => x.Id).ToList();
|
||||||
|
var removed = await _dbFactory.RunWrite(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct));
|
||||||
|
sent = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, e.Message);
|
_logger.LogError(e, e.Message);
|
||||||
badImages.Add(annotation.Name);
|
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||||
}
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (badImages.Any())
|
await Task.Delay(TimeSpan.FromSeconds(5), ct);
|
||||||
{
|
|
||||||
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken);
|
|
||||||
_dbFactory.SaveToDisk();
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _dbFactory.Run(async db =>
|
if (_uiConfig.SilentDetection)
|
||||||
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
return;
|
||||||
|
await _dbFactory.RunWrite(async db =>
|
||||||
|
await db.InsertAsync(new AnnotationQueueRecord
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
DateTime = DateTime.UtcNow,
|
||||||
|
Operation = status,
|
||||||
|
AnnotationNames = annotationNames
|
||||||
|
}, token: cancellationToken));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.CommonSecurity;
|
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
|
||||||
|
|
||||||
public interface IGpsMatcherService
|
|
||||||
{
|
|
||||||
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
|
|
||||||
void StopGpsMatching();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
|
|
||||||
{
|
|
||||||
private const int ZOOM_LEVEL = 18;
|
|
||||||
private const int POINTS_COUNT = 5;
|
|
||||||
private const int DISTANCE_BETWEEN_POINTS_M = 100;
|
|
||||||
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
|
|
||||||
|
|
||||||
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
|
|
||||||
{
|
|
||||||
var currentLat = initialLatitude;
|
|
||||||
var currentLon = initialLongitude;
|
|
||||||
|
|
||||||
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
|
|
||||||
if (Directory.Exists(routeDir))
|
|
||||||
Directory.Delete(routeDir, true);
|
|
||||||
Directory.CreateDirectory(routeDir);
|
|
||||||
|
|
||||||
var routeFiles = new List<string>();
|
|
||||||
foreach (var file in Directory.GetFiles(userRouteDir))
|
|
||||||
{
|
|
||||||
routeFiles.Add(file);
|
|
||||||
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexOffset = 0;
|
|
||||||
while (routeFiles.Any())
|
|
||||||
{
|
|
||||||
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
|
|
||||||
gpsMatcherClient.StartMatching(new StartMatchingEvent
|
|
||||||
{
|
|
||||||
ImagesCount = POINTS_COUNT,
|
|
||||||
Latitude = initialLatitude,
|
|
||||||
Longitude = initialLongitude,
|
|
||||||
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
|
|
||||||
RouteDir = dirConfig.Value.GpsRouteDirectory
|
|
||||||
});
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var result = gpsMatcherClient.GetResult();
|
|
||||||
if (result == null)
|
|
||||||
break;
|
|
||||||
result.Index += indexOffset;
|
|
||||||
await processResult(result);
|
|
||||||
currentLat = result.Latitude;
|
|
||||||
currentLon = result.Longitude;
|
|
||||||
routeFiles.RemoveAt(0);
|
|
||||||
}
|
|
||||||
indexOffset += POINTS_COUNT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopGpsMatching()
|
|
||||||
{
|
|
||||||
gpsMatcherClient.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -9,7 +9,6 @@ using Azaion.Common.DTO;
|
|||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -61,7 +60,7 @@ public class GalleryService(
|
|||||||
{
|
{
|
||||||
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
|
||||||
file.Delete();
|
file.Delete();
|
||||||
await dbFactory.Run(async db =>
|
await dbFactory.RunWrite(async db =>
|
||||||
{
|
{
|
||||||
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
|
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
|
||||||
await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
|
await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
|
||||||
@@ -150,14 +149,14 @@ public class GalleryService(
|
|||||||
if (!existingAnnotations.ContainsKey(fName))
|
if (!existingAnnotations.ContainsKey(fName))
|
||||||
{
|
{
|
||||||
if (missedAnnotations.ContainsKey(fName))
|
if (missedAnnotations.ContainsKey(fName))
|
||||||
Console.WriteLine($"{fName} is already exists! Duplicate!");
|
logger.LogInformation($"{fName} is already exists! Duplicate!");
|
||||||
else
|
else
|
||||||
missedAnnotations.TryAdd(fName, annotation);
|
missedAnnotations.TryAdd(fName, annotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!thumbnails.Contains(fName))
|
if (!thumbnails.Contains(fName))
|
||||||
await CreateThumbnail(annotation, cancellationToken);
|
await CreateThumbnail(annotation, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -169,7 +168,7 @@ public class GalleryService(
|
|||||||
{
|
{
|
||||||
ProgressFn = async num =>
|
ProgressFn = async num =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
|
logger.LogInformation($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
|
||||||
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
||||||
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
|
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -198,24 +197,23 @@ public class GalleryService(
|
|||||||
.Select(x => x.Value)
|
.Select(x => x.Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await dbFactory.Run(async db =>
|
await dbFactory.RunWrite(async db =>
|
||||||
{
|
{
|
||||||
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
|
||||||
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
|
||||||
});
|
});
|
||||||
dbFactory.SaveToDisk();
|
|
||||||
_updateLock.Release();
|
_updateLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default)
|
public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var width = (int)_thumbnailConfig.Size.Width;
|
var width = (int)_thumbnailConfig.Size.Width;
|
||||||
var height = (int)_thumbnailConfig.Size.Height;
|
var height = (int)_thumbnailConfig.Size.Height;
|
||||||
|
|
||||||
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
|
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
|
||||||
|
|
||||||
var bitmap = new Bitmap(width, height);
|
var bitmap = new Bitmap(width, height);
|
||||||
|
|
||||||
@@ -239,11 +237,11 @@ public class GalleryService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
if (annotation.Detections.Any())
|
if (annotation.Detections.Any())
|
||||||
{
|
{
|
||||||
var labelsMinX = labels.Min(x => x.X);
|
var labelsMinX = labels.Min(x => x.Left);
|
||||||
var labelsMaxX = labels.Max(x => x.X + x.Width);
|
var labelsMaxX = labels.Max(x => x.Left + x.Width);
|
||||||
|
|
||||||
var labelsMinY = labels.Min(x => x.Y);
|
var labelsMinY = labels.Min(x => x.Top);
|
||||||
var labelsMaxY = labels.Max(x => x.Y + x.Height);
|
var labelsMaxY = labels.Max(x => x.Top + x.Height);
|
||||||
|
|
||||||
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
|
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
|
||||||
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
|
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
|
||||||
@@ -272,7 +270,7 @@ public class GalleryService(
|
|||||||
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
|
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
|
||||||
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
||||||
|
|
||||||
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
||||||
@@ -282,10 +280,9 @@ public class GalleryService(
|
|||||||
logger.LogError(e, e.Message);
|
logger.LogError(e, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
|
||||||
public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token)
|
|
||||||
{
|
{
|
||||||
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
|
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
|
||||||
|
|
||||||
using var g = Graphics.FromImage(originalImage);
|
using var g = Graphics.FromImage(originalImage);
|
||||||
foreach (var detection in annotation.Detections)
|
foreach (var detection in annotation.Detections)
|
||||||
@@ -294,22 +291,25 @@ public class GalleryService(
|
|||||||
var color = detClass.Color;
|
var color = detClass.Color;
|
||||||
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
||||||
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
|
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
|
||||||
g.DrawRectangle(new Pen(brush, width: 3), (float)det.X, (float)det.Y, (float)det.Width, (float)det.Height);
|
g.DrawRectangle(new Pen(brush, width: 3), (float)det.Left, (float)det.Top, (float)det.Width, (float)det.Height);
|
||||||
|
|
||||||
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
|
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
|
||||||
g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black);
|
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
|
||||||
}
|
}
|
||||||
originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg);
|
|
||||||
|
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
|
||||||
|
if (File.Exists(imagePath))
|
||||||
|
ResilienceExt.WithRetry(() => File.Delete(imagePath));
|
||||||
|
|
||||||
|
originalImage.Save(imagePath, ImageFormat.Jpeg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IGalleryService
|
public interface IGalleryService
|
||||||
{
|
{
|
||||||
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
|
||||||
double ProcessedThumbnailsPercentage { get; set; }
|
Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default);
|
||||||
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
|
|
||||||
Task RefreshThumbnails();
|
Task RefreshThumbnails();
|
||||||
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
||||||
|
Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default);
|
||||||
Task CreateAnnotatedImage(Annotation annotation, CancellationToken token);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class GPSMatcherEventHandler(IGpsMatcherService gpsMatcherService) :
|
||||||
|
INotificationHandler<GPSMatcherResultEvent>,
|
||||||
|
INotificationHandler<GPSMatcherFinishedEvent>
|
||||||
|
{
|
||||||
|
public async Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken) =>
|
||||||
|
await gpsMatcherService.SetGpsResult(result, cancellationToken);
|
||||||
|
|
||||||
|
public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) =>
|
||||||
|
await gpsMatcherService.FinishGPS(notification, cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Azaion.Common.DTO;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public enum MatchTypeEnum
|
||||||
|
{
|
||||||
|
None = -1,
|
||||||
|
MatchTypeSingle = 0,
|
||||||
|
MatchTypeStitched = 1,
|
||||||
|
MatchTypeOpticalFlow = 2,
|
||||||
|
MatchTypeInterpolated = 3,
|
||||||
|
MatchTypeFailure = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GPSMatcherResultEvent : INotification
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
public string Image { get; set; } = null!;
|
||||||
|
public GeoPoint GeoPoint { get; set; } = null!;
|
||||||
|
public int KeyPoints { get; set; }
|
||||||
|
public MatchTypeEnum MatchType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GPSMatcherResultProcessedEvent : GPSMatcherResultEvent
|
||||||
|
{
|
||||||
|
public GeoPoint ProcessedGeoPoint { get; set; } = null!;
|
||||||
|
|
||||||
|
public GPSMatcherResultProcessedEvent() { }
|
||||||
|
|
||||||
|
public GPSMatcherResultProcessedEvent(GPSMatcherResultEvent gpsMatcherResultEvent, GeoPoint processedGeoPoint)
|
||||||
|
{
|
||||||
|
Index = gpsMatcherResultEvent.Index;
|
||||||
|
Image = gpsMatcherResultEvent.Image;
|
||||||
|
GeoPoint = gpsMatcherResultEvent.GeoPoint;
|
||||||
|
KeyPoints = gpsMatcherResultEvent.KeyPoints;
|
||||||
|
MatchType = gpsMatcherResultEvent.MatchType;
|
||||||
|
ProcessedGeoPoint = processedGeoPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class GPSMatcherJobAcceptedEvent : INotification {}
|
||||||
|
|
||||||
|
public class GPSMatcherFinishedEvent : INotification {}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IGpsMatcherService
|
||||||
|
{
|
||||||
|
Task RunGpsMatching(string userRouteDir, GeoPoint geoPoint, CancellationToken detectToken = default);
|
||||||
|
void StopGpsMatching();
|
||||||
|
Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default);
|
||||||
|
Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient,
|
||||||
|
ISatelliteDownloader satelliteTileDownloader,
|
||||||
|
IOptions<DirectoriesConfig> dirConfig,
|
||||||
|
IOptions<GpsDeniedConfig> gpsDeniedConfig,
|
||||||
|
IMediator mediator) : IGpsMatcherService
|
||||||
|
{
|
||||||
|
private readonly DirectoriesConfig _dirConfig = dirConfig.Value;
|
||||||
|
private const int ZOOM_LEVEL = 18;
|
||||||
|
private const int POINTS_COUNT = 10;
|
||||||
|
private const int DISTANCE_BETWEEN_POINTS_M = 140;
|
||||||
|
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
|
||||||
|
private const int MAX_AVG_POINTS = 2;
|
||||||
|
|
||||||
|
private string _routeDir = "";
|
||||||
|
private string _userRouteDir = "";
|
||||||
|
private List<string> _allRouteImages = new();
|
||||||
|
private Dictionary<string, int> _currentRouteImages = new();
|
||||||
|
private GeoPoint _lastGeoPoint = new();
|
||||||
|
private CancellationToken _detectToken;
|
||||||
|
private int _currentIndex;
|
||||||
|
private readonly Queue<Direction> _directions = new();
|
||||||
|
|
||||||
|
|
||||||
|
public async Task RunGpsMatching(string userRouteDir, GeoPoint initGeoPoint, CancellationToken detectToken = default)
|
||||||
|
{
|
||||||
|
_routeDir = Path.Combine(Constants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory);
|
||||||
|
_userRouteDir = userRouteDir;
|
||||||
|
|
||||||
|
_allRouteImages = Directory.GetFiles(userRouteDir)
|
||||||
|
.OrderBy(x => x).ToList();
|
||||||
|
_lastGeoPoint = initGeoPoint;
|
||||||
|
_detectToken = detectToken;
|
||||||
|
await StartMatchingRound(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartMatchingRound(int startIndex)
|
||||||
|
{
|
||||||
|
//empty route dir
|
||||||
|
if (Directory.Exists(_routeDir))
|
||||||
|
Directory.Delete(_routeDir, true);
|
||||||
|
Directory.CreateDirectory(_routeDir);
|
||||||
|
|
||||||
|
_currentRouteImages = _allRouteImages
|
||||||
|
.Skip(startIndex)
|
||||||
|
.Take(POINTS_COUNT)
|
||||||
|
.Select((fullName, index) =>
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(fullName);
|
||||||
|
File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename));
|
||||||
|
return new { Filename = Path.GetFileNameWithoutExtension(fullName), Index = startIndex + index };
|
||||||
|
})
|
||||||
|
.ToDictionary(x => x.Filename, x => x.Index);
|
||||||
|
|
||||||
|
await satelliteTileDownloader.GetTiles(_lastGeoPoint, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken);
|
||||||
|
await gpsMatcherClient.StartMatching(new StartMatchingEvent
|
||||||
|
{
|
||||||
|
ImagesCount = POINTS_COUNT,
|
||||||
|
GeoPoint = _lastGeoPoint,
|
||||||
|
SatelliteImagesDir = _dirConfig.GpsSatDirectory,
|
||||||
|
RouteDir = _dirConfig.GpsRouteDirectory
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopGpsMatching()
|
||||||
|
{
|
||||||
|
gpsMatcherClient.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default)
|
||||||
|
{
|
||||||
|
_currentIndex = _currentRouteImages[result.Image];
|
||||||
|
_currentRouteImages.Remove(result.Image);
|
||||||
|
|
||||||
|
if (result.KeyPoints >= gpsDeniedConfig.Value.MinKeyPoints)
|
||||||
|
{
|
||||||
|
var direction = _lastGeoPoint.DirectionTo(result.GeoPoint);
|
||||||
|
_directions.Enqueue(direction);
|
||||||
|
if (_directions.Count > MAX_AVG_POINTS)
|
||||||
|
_directions.Dequeue();
|
||||||
|
_lastGeoPoint = result.GeoPoint;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var direction = new Direction(_directions.Average(x => x.Distance), _directions.Average(x => x.Azimuth));
|
||||||
|
_lastGeoPoint = _lastGeoPoint.GoDirection(direction);
|
||||||
|
}
|
||||||
|
await mediator.Publish(new GPSMatcherResultProcessedEvent(result, _lastGeoPoint), detectToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count)
|
||||||
|
await StartMatchingRound(_currentIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.Events;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NetMQ;
|
||||||
|
using NetMQ.Sockets;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IGpsMatcherClient : IDisposable
|
||||||
|
{
|
||||||
|
Task StartMatching(StartMatchingEvent startEvent);
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StartMatchingEvent
|
||||||
|
{
|
||||||
|
public string RouteDir { get; set; } = null!;
|
||||||
|
public string SatelliteImagesDir { get; set; } = null!;
|
||||||
|
public int ImagesCount { get; set; }
|
||||||
|
public GeoPoint GeoPoint { get; set; } = null!;
|
||||||
|
public int Altitude { get; set; } = 400;
|
||||||
|
public double CameraSensorWidth { get; set; } = 23.5;
|
||||||
|
public double CameraFocalLength { get; set; } = 24;
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{GeoPoint.Lat},{GeoPoint.Lon},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsMatcherClient : IGpsMatcherClient
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
private readonly ILogger<GpsMatcherClient> _logger;
|
||||||
|
private readonly string _requestAddress;
|
||||||
|
private readonly RequestSocket _requestSocket = new();
|
||||||
|
private readonly string _subscriberAddress;
|
||||||
|
private readonly SubscriberSocket _subscriberSocket = new();
|
||||||
|
private readonly NetMQPoller _poller = new();
|
||||||
|
|
||||||
|
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
_logger = logger;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = Constants.ExternalGpsDeniedPath,
|
||||||
|
Arguments = $"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
|
||||||
|
WorkingDirectory = Constants.EXTERNAL_GPS_DENIED_FOLDER,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}";
|
||||||
|
_requestSocket.Connect(_requestAddress);
|
||||||
|
|
||||||
|
_subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqReceiverPort}";
|
||||||
|
_subscriberSocket.Connect(_subscriberAddress);
|
||||||
|
_subscriberSocket.Subscribe("");
|
||||||
|
_subscriberSocket.ReceiveReady += async (sender, e) => await ProcessClientCommand(sender, e);
|
||||||
|
|
||||||
|
_poller.Add(_subscriberSocket);
|
||||||
|
_poller.RunAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessClientCommand(object? sender, NetMQSocketEventArgs e)
|
||||||
|
{
|
||||||
|
while (e.Socket.TryReceiveFrameString(TimeSpan.FromMilliseconds(100), out var str))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (str)
|
||||||
|
{
|
||||||
|
case "FINISHED":
|
||||||
|
await _mediator.Publish(new GPSMatcherFinishedEvent());
|
||||||
|
break;
|
||||||
|
case "OK":
|
||||||
|
await _mediator.Publish(new GPSMatcherJobAcceptedEvent());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
var parts = str.Split(',');
|
||||||
|
if (parts.Length != 6)
|
||||||
|
throw new Exception("Matching Result Failed");
|
||||||
|
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(parts[1]);
|
||||||
|
await _mediator.Publish(new GPSMatcherResultEvent
|
||||||
|
{
|
||||||
|
Index = int.Parse(parts[0]),
|
||||||
|
Image = filename,
|
||||||
|
GeoPoint = new GeoPoint(double.Parse(parts[2]), double.Parse(parts[3])),
|
||||||
|
KeyPoints = int.Parse(parts[4]),
|
||||||
|
MatchType = Enum.TryParse<MatchTypeEnum>(parts[5], out var type) ? type : MatchTypeEnum.None
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartMatching(StartMatchingEvent e)
|
||||||
|
{
|
||||||
|
_requestSocket.SendFrame(e.ToString());
|
||||||
|
_requestSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(300), out var response);
|
||||||
|
if (response != "OK")
|
||||||
|
{
|
||||||
|
_logger.LogError(response);
|
||||||
|
await _mediator.Publish(new SetStatusTextEvent(response ?? "", true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop() => _requestSocket.SendFrame("STOP");
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_poller.Stop();
|
||||||
|
_poller.Dispose();
|
||||||
|
|
||||||
|
_requestSocket.SendFrame("EXIT");
|
||||||
|
_requestSocket.Disconnect(_requestAddress);
|
||||||
|
_requestSocket.Dispose();
|
||||||
|
|
||||||
|
_subscriberSocket.Disconnect(_subscriberAddress);
|
||||||
|
_subscriberSocket.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using Azaion.CommonSecurity;
|
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NetMQ;
|
|
||||||
using NetMQ.Sockets;
|
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
|
||||||
|
|
||||||
public interface IGpsMatcherClient
|
|
||||||
{
|
|
||||||
|
|
||||||
void StartMatching(StartMatchingEvent startEvent);
|
|
||||||
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
|
|
||||||
void Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StartMatchingEvent
|
|
||||||
{
|
|
||||||
public string RouteDir { get; set; } = null!;
|
|
||||||
public string SatelliteImagesDir { get; set; } = null!;
|
|
||||||
public int ImagesCount { get; set; }
|
|
||||||
public double Latitude { get; set; }
|
|
||||||
public double Longitude { get; set; }
|
|
||||||
public string ProcessingType { get; set; } = "cuda";
|
|
||||||
public int Altitude { get; set; } = 400;
|
|
||||||
public double CameraSensorWidth { get; set; } = 23.5;
|
|
||||||
public double CameraFocalLength { get; set; } = 24;
|
|
||||||
|
|
||||||
public override string ToString() =>
|
|
||||||
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GpsMatcherClient : IGpsMatcherClient
|
|
||||||
{
|
|
||||||
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
|
|
||||||
private readonly RequestSocket _requestSocket = new();
|
|
||||||
private readonly SubscriberSocket _subscriberSocket = new();
|
|
||||||
|
|
||||||
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
|
|
||||||
{
|
|
||||||
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
|
|
||||||
Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var process = new Process();
|
|
||||||
process.StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = SecurityConstants.ExternalGpsDeniedPath,
|
|
||||||
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
|
|
||||||
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
|
|
||||||
//RedirectStandardOutput = true,
|
|
||||||
//RedirectStandardError = true,
|
|
||||||
//CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
|
||||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
|
||||||
//process.Start();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
//throw;
|
|
||||||
}
|
|
||||||
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
|
|
||||||
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
|
|
||||||
_subscriberSocket.Subscribe("");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartMatching(StartMatchingEvent e)
|
|
||||||
{
|
|
||||||
_requestSocket.SendFrame(e.ToString());
|
|
||||||
var response = _requestSocket.ReceiveFrameString();
|
|
||||||
if (response != "OK")
|
|
||||||
throw new Exception("Start Matching Failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var tryNum = 0;
|
|
||||||
while (!ct.IsCancellationRequested && tryNum++ < retries)
|
|
||||||
{
|
|
||||||
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
|
|
||||||
continue;
|
|
||||||
if (update == "FINISHED")
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var parts = update.Split(',');
|
|
||||||
if (parts.Length != 5)
|
|
||||||
throw new Exception("Matching Result Failed");
|
|
||||||
|
|
||||||
return new GpsMatchResult
|
|
||||||
{
|
|
||||||
Index = int.Parse(parts[0]),
|
|
||||||
Image = parts[1],
|
|
||||||
Latitude = double.Parse(parts[2]),
|
|
||||||
Longitude = double.Parse(parts[3]),
|
|
||||||
MatchType = parts[4]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ct.IsCancellationRequested)
|
|
||||||
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
_requestSocket.SendFrame("STOP");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using MediatR;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NetMQ;
|
||||||
|
using NetMQ.Sockets;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services.Inference;
|
||||||
|
|
||||||
|
public interface IInferenceClient : IDisposable
|
||||||
|
{
|
||||||
|
void Send(RemoteCommand create);
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceClient : IInferenceClient
|
||||||
|
{
|
||||||
|
private readonly ILogger<InferenceClient> _logger;
|
||||||
|
|
||||||
|
private readonly DealerSocket _dealer = new();
|
||||||
|
private readonly NetMQPoller _poller = new();
|
||||||
|
private readonly Guid _clientId = Guid.NewGuid();
|
||||||
|
private readonly InferenceClientConfig _inferenceClientConfig;
|
||||||
|
private readonly LoaderClientConfig _loaderClientConfig;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
|
||||||
|
IMediator mediator,
|
||||||
|
IOptions<LoaderClientConfig> loaderConfig)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_inferenceClientConfig = inferenceConfig.Value;
|
||||||
|
_loaderClientConfig = loaderConfig.Value;
|
||||||
|
_mediator = mediator;
|
||||||
|
Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = Constants.EXTERNAL_INFERENCE_PATH,
|
||||||
|
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
||||||
|
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
|
||||||
|
|
||||||
|
_dealer.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket);
|
||||||
|
_poller.Add(_dealer);
|
||||||
|
_ = Task.Run(() => _poller.RunAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes))
|
||||||
|
{
|
||||||
|
if (bytes.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var remoteCommand = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
|
||||||
|
switch (remoteCommand.CommandType)
|
||||||
|
{
|
||||||
|
case CommandType.InferenceData:
|
||||||
|
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
|
||||||
|
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
|
||||||
|
break;
|
||||||
|
case CommandType.InferenceStatus:
|
||||||
|
var statusEvent = MessagePackSerializer.Deserialize<InferenceStatusEvent>(remoteCommand.Data, cancellationToken: ct);
|
||||||
|
await _mediator.Publish(statusEvent, ct);
|
||||||
|
break;
|
||||||
|
case CommandType.InferenceDone:
|
||||||
|
await _mediator.Publish(new InferenceDoneEvent(), ct);
|
||||||
|
break;
|
||||||
|
case CommandType.AIAvailabilityResult:
|
||||||
|
var aiAvailabilityStatus = MessagePackSerializer.Deserialize<AIAvailabilityStatusEvent>(remoteCommand.Data, cancellationToken: ct);
|
||||||
|
await _mediator.Publish(aiAvailabilityStatus, ct);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop() =>
|
||||||
|
Send(RemoteCommand.Create(CommandType.StopInference));
|
||||||
|
|
||||||
|
public void Send(RemoteCommand command) =>
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_poller.Stop();
|
||||||
|
_poller.Dispose();
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
|
||||||
|
_dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
|
||||||
|
_dealer.Close();
|
||||||
|
_dealer.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services.Inference;
|
||||||
|
|
||||||
|
public interface IInferenceService
|
||||||
|
{
|
||||||
|
Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default);
|
||||||
|
CancellationTokenSource InferenceCancelTokenSource { get; set; }
|
||||||
|
CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; }
|
||||||
|
void StopInference();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHOULD BE ONLY ONE INSTANCE OF InferenceService. Do not add ANY NotificationHandler to it!
|
||||||
|
// _inferenceCancelTokenSource should be created only once.
|
||||||
|
public class InferenceService : IInferenceService
|
||||||
|
{
|
||||||
|
private readonly IInferenceClient _client;
|
||||||
|
private readonly IAzaionApi _azaionApi;
|
||||||
|
private readonly IOptions<AIRecognitionConfig> _aiConfigOptions;
|
||||||
|
|
||||||
|
public InferenceService(IInferenceClient client,
|
||||||
|
IAzaionApi azaionApi,
|
||||||
|
IOptions<AIRecognitionConfig> aiConfigOptions)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_azaionApi = azaionApi;
|
||||||
|
_aiConfigOptions = aiConfigOptions;
|
||||||
|
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CancellationTokenSource InferenceCancelTokenSource { get; set; } = new();
|
||||||
|
public CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } = new();
|
||||||
|
|
||||||
|
private async Task CheckAIAvailabilityStatus()
|
||||||
|
{
|
||||||
|
CheckAIAvailabilityTokenSource = new CancellationTokenSource();
|
||||||
|
while (!CheckAIAvailabilityTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_client.Send(RemoteCommand.Create(CommandType.AIAvailabilityCheck));
|
||||||
|
await Task.Delay(10000, CheckAIAvailabilityTokenSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
InferenceCancelTokenSource = new CancellationTokenSource();
|
||||||
|
_client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials));
|
||||||
|
|
||||||
|
var aiConfig = _aiConfigOptions.Value;
|
||||||
|
aiConfig.Paths = mediaPaths;
|
||||||
|
aiConfig.Altitude = (double)cameraConfig.Altitude;
|
||||||
|
aiConfig.CameraFocalLength = (double)cameraConfig.CameraFocalLength;
|
||||||
|
aiConfig.CameraSensorWidth = (double)cameraConfig.CameraSensorWidth;
|
||||||
|
_client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
|
||||||
|
|
||||||
|
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, InferenceCancelTokenSource.Token);
|
||||||
|
await combinedTokenSource.Token.AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInference() => _client.Stop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.Events;
|
||||||
|
using MediatR;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services.Inference;
|
||||||
|
|
||||||
|
public class InferenceServiceEventHandler(IInferenceService inferenceService, IAnnotationService annotationService, IMediator mediator) :
|
||||||
|
INotificationHandler<InferenceDataEvent>,
|
||||||
|
INotificationHandler<InferenceStatusEvent>,
|
||||||
|
INotificationHandler<InferenceDoneEvent>
|
||||||
|
{
|
||||||
|
|
||||||
|
public async Task Handle(InferenceDataEvent e, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var annotation = await annotationService.SaveAnnotation(e.AnnotationImage, ct);
|
||||||
|
await mediator.Publish(new AnnotationAddedEvent(annotation), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(InferenceStatusEvent e, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Publish(new SetStatusTextEvent($"{e.MediaName}: {e.DetectionsCount} detections"), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(InferenceDoneEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await inferenceService.InferenceCancelTokenSource.CancelAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
using MediatR;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services.Inference;
|
||||||
|
|
||||||
|
public class InferenceDataEvent(AnnotationImage annotationImage) : INotification
|
||||||
|
{
|
||||||
|
public AnnotationImage AnnotationImage { get; set; } = annotationImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class InferenceStatusEvent : INotification
|
||||||
|
{
|
||||||
|
[Key("mn")]
|
||||||
|
public string? MediaName { get; set; }
|
||||||
|
|
||||||
|
[Key("dc")]
|
||||||
|
public int DetectionsCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceDoneEvent : INotification;
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Azaion.Common.Database;
|
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.CommonSecurity;
|
|
||||||
using Azaion.CommonSecurity.DTO.Commands;
|
|
||||||
using Azaion.CommonSecurity.Services;
|
|
||||||
using MessagePack;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Azaion.Common.Services;
|
|
||||||
|
|
||||||
public interface IInferenceService
|
|
||||||
{
|
|
||||||
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default);
|
|
||||||
void StopInference();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IAzaionApi azaionApi, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
|
|
||||||
{
|
|
||||||
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
|
|
||||||
{
|
|
||||||
client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials));
|
|
||||||
var aiConfig = aiConfigOptions.Value;
|
|
||||||
|
|
||||||
aiConfig.Paths = mediaPaths;
|
|
||||||
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
|
|
||||||
|
|
||||||
while (!detectToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var bytes = client.GetBytes(ct: detectToken);
|
|
||||||
if (bytes == null)
|
|
||||||
throw new Exception("Can't get bytes from inference client");
|
|
||||||
|
|
||||||
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
|
|
||||||
return;
|
|
||||||
|
|
||||||
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken);
|
|
||||||
|
|
||||||
await processAnnotation(annotationImage);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogError(e, e.Message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopInference()
|
|
||||||
{
|
|
||||||
client.Send(RemoteCommand.Create(CommandType.StopInference));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using MessagePack;
|
||||||
|
using NetMQ;
|
||||||
|
using NetMQ.Sockets;
|
||||||
|
using Serilog;
|
||||||
|
using Exception = System.Exception;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DealerSocket _dealer = new();
|
||||||
|
private readonly Guid _clientId = Guid.NewGuid();
|
||||||
|
|
||||||
|
public void StartClient()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = Constants.EXTERNAL_LOADER_PATH,
|
||||||
|
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.Error(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Connect()
|
||||||
|
{
|
||||||
|
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
||||||
|
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Login(ApiCredentials credentials)
|
||||||
|
{
|
||||||
|
var result = SendCommand(RemoteCommand.Create(CommandType.Login, credentials));
|
||||||
|
if (result.CommandType != CommandType.Ok)
|
||||||
|
throw new Exception(result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryStream LoadFile(string filename, string folder)
|
||||||
|
{
|
||||||
|
var result = SendCommand(RemoteCommand.Create(CommandType.Load, new LoadFileData(filename, folder)));
|
||||||
|
if (result.Data?.Length == 0)
|
||||||
|
throw new Exception($"Can't load {filename}. Returns 0 bytes");
|
||||||
|
return new MemoryStream(result.Data!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RemoteCommand SendCommand(RemoteCommand command, int retryCount = 50, int retryDelayMs = 800)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
||||||
|
|
||||||
|
var tryNum = 0;
|
||||||
|
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
|
||||||
|
{
|
||||||
|
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
|
||||||
|
continue;
|
||||||
|
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
|
||||||
|
if (res.CommandType == CommandType.Error)
|
||||||
|
throw new Exception(res.Message);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"Sent {command} {retryCount} times, with wait time {retryDelayMs}ms for each call. No response from client.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.Error(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_dealer.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ using System.Net.Http;
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.CommonSecurity;
|
using Azaion.CommonSecurity;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -19,14 +20,15 @@ namespace Azaion.Common.Services;
|
|||||||
|
|
||||||
public interface ISatelliteDownloader
|
public interface ISatelliteDownloader
|
||||||
{
|
{
|
||||||
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
|
Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SatelliteDownloader(
|
public class SatelliteDownloader(
|
||||||
ILogger<SatelliteDownloader> logger,
|
ILogger<SatelliteDownloader> logger,
|
||||||
IOptions<MapConfig> mapConfig,
|
IOptions<MapConfig> mapConfig,
|
||||||
IOptions<DirectoriesConfig> directoriesConfig,
|
IOptions<DirectoriesConfig> directoriesConfig,
|
||||||
IHttpClientFactory httpClientFactory)
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IMediator mediator)
|
||||||
: ISatelliteDownloader
|
: ISatelliteDownloader
|
||||||
{
|
{
|
||||||
private const int INPUT_TILE_SIZE = 256;
|
private const int INPUT_TILE_SIZE = 256;
|
||||||
@@ -41,19 +43,30 @@ public class SatelliteDownloader(
|
|||||||
private const int OUTPUT_TILE_SIZE = 512;
|
private const int OUTPUT_TILE_SIZE = 512;
|
||||||
|
|
||||||
private readonly string _apiKey = mapConfig.Value.ApiKey;
|
private readonly string _apiKey = mapConfig.Value.ApiKey;
|
||||||
private readonly string _satDirectory = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
|
private readonly string _satDirectory = Path.Combine(Constants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
|
||||||
|
|
||||||
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
public async Task GetTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
await mediator.Publish(new SetStatusTextEvent(
|
||||||
|
$"Завантажуються супутникові зображення по координатах: центр: " +
|
||||||
|
$"lat: {centerGeoPoint.Lat:F3} lon: {centerGeoPoint.Lon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token);
|
||||||
//empty Satellite directory
|
//empty Satellite directory
|
||||||
if (Directory.Exists(_satDirectory))
|
if (Directory.Exists(_satDirectory))
|
||||||
Directory.Delete(_satDirectory, true);
|
Directory.Delete(_satDirectory, true);
|
||||||
Directory.CreateDirectory(_satDirectory);
|
Directory.CreateDirectory(_satDirectory);
|
||||||
|
|
||||||
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
|
var dtRes = await DownloadTiles(centerGeoPoint, radiusM, zoomLevel, token);
|
||||||
var image = ComposeTiles(downloadTilesResult.Tiles, token);
|
await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token);
|
||||||
if (image != null)
|
var image = ComposeTiles(dtRes.Tiles, token);
|
||||||
await SplitToTiles(image, downloadTilesResult, token);
|
if (image == null)
|
||||||
|
return;
|
||||||
|
// Save big map. Uncomment when MapHandler with custom pick images would be ready
|
||||||
|
// var outputFilename = Path.Combine(_satDirectory,
|
||||||
|
// $"map_tl_{dtRes.LatMax:F6}_{dtRes.LonMin:F6}_br_{dtRes.LatMin:F6}_{dtRes.LonMax:F6}.tif"
|
||||||
|
// );
|
||||||
|
// await image.SaveAsTiffAsync(outputFilename, token);
|
||||||
|
await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token);
|
||||||
|
await SplitToTiles(image, dtRes, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
|
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
|
||||||
@@ -140,7 +153,7 @@ public class SatelliteDownloader(
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error while loading tile: {tileData}");
|
logger.LogError($"Error while loading tile: {tileData}");
|
||||||
}
|
}
|
||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
@@ -172,9 +185,9 @@ public class SatelliteDownloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
private async Task<DownloadTilesResult> DownloadTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM);
|
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM);
|
||||||
|
|
||||||
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
|
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
|
||||||
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
|
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public class TileResult(CanvasLabel tile, List<CanvasLabel> detections)
|
||||||
|
{
|
||||||
|
public CanvasLabel Tile { get; set; } = tile;
|
||||||
|
public List<CanvasLabel> Detections { get; set; } = detections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TileProcessor
|
||||||
|
{
|
||||||
|
public const int BORDER = 10;
|
||||||
|
|
||||||
|
public static List<TileResult> Split(Size originalSize, List<CanvasLabel> detections, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var results = new List<TileResult>();
|
||||||
|
var processingDetectionList = new List<CanvasLabel>(detections);
|
||||||
|
|
||||||
|
while (processingDetectionList.Count > 0 && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var topMostDetection = processingDetectionList
|
||||||
|
.OrderBy(d => d.Top)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
var result = GetDetectionsInTile(originalSize, topMostDetection, processingDetectionList);
|
||||||
|
processingDetectionList.RemoveAll(x => result.Detections.Contains(x));
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TileResult GetDetectionsInTile(Size originalSize, CanvasLabel startDet, List<CanvasLabel> allDetections)
|
||||||
|
{
|
||||||
|
var tile = new CanvasLabel(startDet.Left, startDet.Right, startDet.Top, startDet.Bottom);
|
||||||
|
var maxSize = new List<double> { startDet.Width + BORDER, startDet.Height + BORDER, Constants.AI_TILE_SIZE_DEFAULT }.Max();
|
||||||
|
var selectedDetections = new List<CanvasLabel>{startDet};
|
||||||
|
|
||||||
|
foreach (var det in allDetections)
|
||||||
|
{
|
||||||
|
if (det == startDet)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var commonTile = new CanvasLabel(
|
||||||
|
left: Math.Min(tile.Left, det.Left),
|
||||||
|
right: Math.Max(tile.Right, det.Right),
|
||||||
|
top: Math.Min(tile.Top, det.Top),
|
||||||
|
bottom: Math.Max(tile.Bottom, det.Bottom)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (commonTile.Width + BORDER > maxSize || commonTile.Height + BORDER > maxSize)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tile = commonTile;
|
||||||
|
selectedDetections.Add(det);
|
||||||
|
}
|
||||||
|
|
||||||
|
// boundary-aware centering
|
||||||
|
var centerX = selectedDetections.Average(x => x.CenterX);
|
||||||
|
var centerY = selectedDetections.Average(d => d.CenterY);
|
||||||
|
tile.Width = maxSize;
|
||||||
|
tile.Height = maxSize;
|
||||||
|
tile.Left = Math.Max(0, Math.Min(originalSize.Width - maxSize, centerX - tile.Width / 2.0));
|
||||||
|
tile.Top = Math.Max(0, Math.Min(originalSize.Height - maxSize, centerY - tile.Height / 2.0));
|
||||||
|
|
||||||
|
return new TileResult( tile, selectedDetections);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
|
|
||||||
<PackageReference Include="MessagePack" Version="3.1.0" />
|
|
||||||
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
|
||||||
<PackageReference Include="NetMQ" Version="4.0.1.13" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using MessagePack;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
[MessagePackObject]
|
|
||||||
public class ApiCredentials(string email, string password) : EventArgs
|
|
||||||
{
|
|
||||||
[Key(nameof(Email))]
|
|
||||||
public string Email { get; set; } = email;
|
|
||||||
|
|
||||||
[Key(nameof(Password))]
|
|
||||||
public string Password { get; set; } = password;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
public class HardwareInfo
|
|
||||||
{
|
|
||||||
public string CPU { get; set; } = null!;
|
|
||||||
public string GPU { get; set; } = null!;
|
|
||||||
public string MacAddress { get; set; } = null!;
|
|
||||||
public string Memory { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity;
|
|
||||||
|
|
||||||
public class SecurityConstants
|
|
||||||
{
|
|
||||||
public const string CONFIG_PATH = "config.json";
|
|
||||||
|
|
||||||
public const string DUMMY_DIR = "dummy";
|
|
||||||
|
|
||||||
#region ExternalClientsConfig
|
|
||||||
|
|
||||||
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
|
|
||||||
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
|
|
||||||
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
|
|
||||||
|
|
||||||
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
|
|
||||||
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
|
|
||||||
|
|
||||||
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
|
|
||||||
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
|
|
||||||
|
|
||||||
public const int DEFAULT_RETRY_COUNT = 25;
|
|
||||||
public const int DEFAULT_TIMEOUT_SECONDS = 5;
|
|
||||||
|
|
||||||
# region Cache keys
|
|
||||||
|
|
||||||
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
|
|
||||||
public const string HARDWARE_INFO_KEY = "HardwareInfo";
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
public static readonly SecureAppConfig DefaultSecureAppConfig = new()
|
|
||||||
{
|
|
||||||
InferenceClientConfig = new InferenceClientConfig
|
|
||||||
{
|
|
||||||
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
|
|
||||||
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
|
|
||||||
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
|
||||||
RetryCount = DEFAULT_RETRY_COUNT
|
|
||||||
},
|
|
||||||
GpsDeniedClientConfig = new GpsDeniedClientConfig
|
|
||||||
{
|
|
||||||
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
|
|
||||||
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
|
|
||||||
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
|
||||||
RetryCount = DEFAULT_RETRY_COUNT,
|
|
||||||
},
|
|
||||||
DirectoriesConfig = new DirectoriesConfig
|
|
||||||
{
|
|
||||||
ApiResourcesDirectory = ""
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#endregion ExternalClientsConfig
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Azaion.CommonSecurity.DTO.Commands;
|
|
||||||
using MessagePack;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using NetMQ;
|
|
||||||
using NetMQ.Sockets;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
|
||||||
|
|
||||||
public interface IInferenceClient
|
|
||||||
{
|
|
||||||
void Send(RemoteCommand create);
|
|
||||||
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
|
|
||||||
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
|
|
||||||
void Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InferenceClient : IInferenceClient
|
|
||||||
{
|
|
||||||
private readonly DealerSocket _dealer = new();
|
|
||||||
private readonly Guid _clientId = Guid.NewGuid();
|
|
||||||
private readonly InferenceClientConfig _inferenceClientConfig;
|
|
||||||
|
|
||||||
public InferenceClient(IOptions<InferenceClientConfig> config)
|
|
||||||
{
|
|
||||||
_inferenceClientConfig = config.Value;
|
|
||||||
Start();
|
|
||||||
_ = Task.Run(ProcessClientCommands);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var process = new Process();
|
|
||||||
process.StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
|
|
||||||
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
|
|
||||||
//RedirectStandardOutput = true,
|
|
||||||
//RedirectStandardError = true,
|
|
||||||
//CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
|
||||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
|
||||||
process.Start();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
//throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
|
||||||
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
|
|
||||||
}
|
|
||||||
private async Task ProcessClientCommands()
|
|
||||||
{
|
|
||||||
//TODO: implement always on ready to client's requests. Utilize RemoteCommand
|
|
||||||
|
|
||||||
await Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop()
|
|
||||||
{
|
|
||||||
if (!_dealer.IsDisposed)
|
|
||||||
{
|
|
||||||
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
|
|
||||||
_dealer.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Send(RemoteCommand command)
|
|
||||||
{
|
|
||||||
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
|
||||||
}
|
|
||||||
|
|
||||||
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
|
|
||||||
{
|
|
||||||
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
|
|
||||||
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var tryNum = 0;
|
|
||||||
while (!ct.IsCancellationRequested && tryNum < retries)
|
|
||||||
{
|
|
||||||
tryNum++;
|
|
||||||
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ct.IsCancellationRequested)
|
|
||||||
throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each");
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user