mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 22:06:30 +00:00
Compare commits
361 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 | |||
| 6fce10eb39 | |||
| 522af51a8d | |||
| edd803c304 | |||
| a5fcb0988b | |||
| 66bfe474c2 | |||
| f2b57dccc0 | |||
| c5e81ebcc6 | |||
| cf563571c8 | |||
| dae342b70e | |||
| d02550f5a0 | |||
| 87ceaa805b | |||
| f41d49bd26 | |||
| 58cd0b06e9 | |||
| d92da6afa4 | |||
| 4456e52207 | |||
| 16c264ec9a | |||
| cea1c69fec | |||
| 6e1e6d903d | |||
| 46ec7fddc4 | |||
| dd5521dc3b | |||
| bfeb888c55 | |||
| 9ae9265ff6 | |||
| f2c5490ff3 | |||
| 27b90f2a71 | |||
| 8df405c898 | |||
| e0fd55d908 | |||
| d1520109ef | |||
| 81620888f2 | |||
| 5c3fa80a17 | |||
| e82c3f5bcb | |||
| f7dd087fad | |||
| 64c99d88ec | |||
| 7fff1d9af4 | |||
| 66632a97c1 | |||
| e6ab8bde47 | |||
| 80e1877c18 | |||
| e6ec904657 | |||
| 495268e56a | |||
| c3e4b741c8 | |||
| 5c87f536c1 | |||
| d5e7a28964 | |||
| 85f4d47318 | |||
| 006499c2ee | |||
| 568901edb6 | |||
| 8bf9d1d7e2 | |||
| 79770dc7fa | |||
| 22f047bbe5 | |||
| 52c724eac6 | |||
| 8d9fe62c53 | |||
| 41b96e2ce5 | |||
| b8b4a33f9f | |||
| b937ed8051 | |||
| f49c4e9d37 | |||
| 961c750f1f | |||
| 1acfab9b87 | |||
| 2766be732d | |||
| bb7eec1d5d | |||
| 7fc51e19ee | |||
| 4703c73a24 | |||
| 61ca5c2899 | |||
| b776576b76 | |||
| eddd3f2052 | |||
| 838cd1b8ec | |||
| bfe620d85e | |||
| 0602b0c6f2 | |||
| d547a4007e | |||
| a1f46799c7 | |||
| ccc598d746 | |||
| c83ed53672 | |||
| 7985c298a6 | |||
| 7343e9d630 | |||
| f42646bc72 | |||
| 78caeac943 | |||
| efde78ad93 | |||
| 5d0e49b6fd | |||
| 514c275f3a | |||
| 41c2ed37c1 | |||
| 8574e10b52 | |||
| 44bef40d9b | |||
| f0f6e05b0d | |||
| 0c0cc1bb83 | |||
| 24324b5ffd | |||
| 71e8f088a0 | |||
| 08f93c775b | |||
| 98d99ec7be | |||
| 604e3d132e | |||
| 366aab294e | |||
| 154b5cdcf6 | |||
| 6c49b63e8e | |||
| 6be07693cc | |||
| c1a755c477 | |||
| 23f9ff16a4 | |||
| b165aa3ede | |||
| 05b830b7fb | |||
| 5b143d38ce | |||
| 53ecfb3cad | |||
| fcda29fd49 | |||
| d5057dd86c | |||
| 7de0fe363b | |||
| ea71ec2add | |||
| f100ed638d | |||
| 30d97c6244 | |||
| 1277ebffc6 | |||
| 926345efaf | |||
| 348eca5080 | |||
| 2d93813519 | |||
| c999acd120 | |||
| 44769ca422 | |||
| 6a336e90c5 | |||
| 2ad40e165d | |||
| 83e5eb04e3 | |||
| c5df76e8ce | |||
| ea9730ff3e | |||
| 6f00dfdd42 | |||
| 6d17632c23 | |||
| 0153634166 | |||
| cc86ba795b | |||
| 0327459d1a | |||
| 0dbf25be65 | |||
| 4c12a3244f | |||
| 42f1720172 | |||
| 08e6e2e0c3 | |||
| 4ee901e533 | |||
| 99f3a973a8 | |||
| 5d44eb89e1 | |||
| e01b7e4eac | |||
| 66cc529ffe | |||
| edfdc00807 | |||
| 994528717c | |||
| e6be7d6d15 | |||
| 40f394ac3e | |||
| bdd4262d50 | |||
| e6bfb221e7 | |||
| 970daf84db | |||
| 756806bdeb | |||
| 6ac1fdb14b | |||
| c2b02b8e69 | |||
| 9ef30ea661 | |||
| b429445512 | |||
| 269ba43d7a | |||
| 24442869c0 | |||
| 04c7b124ed | |||
| 9b50c17133 | |||
| 472ed6533e | |||
| 7842fe1067 | |||
| 59eb39d447 | |||
| 0bf0cb94f9 | |||
| b0b1a7daeb | |||
| db44ef02f1 | |||
| 071856d2db | |||
| 506b2ba6de | |||
| 1b4901d568 | |||
| a1aedd7332 | |||
| 28069f63f9 | |||
| cf01e5d952 | |||
| 1c4bdabfb5 | |||
| ae83bc8542 | |||
| b3db108f59 | |||
| 3299192f86 | |||
| 2d83aa06b9 | |||
| ce22620e58 | |||
| a1ee077e0a | |||
| 4bca61d859 | |||
| 42a0c37913 | |||
| 0f9352186e | |||
| 47a925f5af | |||
| 7ebe600adf | |||
| 99a723cfe6 | |||
| e3db942e6b | |||
| be8e8bdc56 | |||
| 4e2d3c7e4f | |||
| 4d1f72f6f9 | |||
| 28d8cc7fcc | |||
| 046cc0eac6 | |||
| 0274dd65a4 | |||
| babcbc0fc7 | |||
| 47aa8b862b | |||
| 5ff4ee58b9 | |||
| 4ae25ceecf | |||
| 77151b54e9 | |||
| e9a44e368d | |||
| e798af470b | |||
| c68c293448 | |||
| 70148bdfdf | |||
| d42409de7d | |||
| 277aaf09b0 | |||
| 0c66607ed7 | |||
| 0237e279a5 | |||
| 1287c13516 | |||
| dd42292eee | |||
| 73f0e10ca8 | |||
| 6109dc18f2 | |||
| d0bceae0dc | |||
| 80de2ad4d0 | |||
| ca1682a86e | |||
| 36b3bf1712 | |||
| c9800107a6 | |||
| 83ae6a0ae9 | |||
| e182547dc8 | |||
| b21f8e320f | |||
| e0c88bd8fb | |||
| 73c2ab5374 | |||
| 6429ad62c2 | |||
| 32f9de3c71 | |||
| 099f9cf52b | |||
| 33070b90bf | |||
| 06f527e6c3 | |||
| f26b5ac8a2 | |||
| 4e6624ee58 | |||
| dc44340f67 | |||
| ee94d2f5db | |||
| aa3b66071f | |||
| 792abce8c4 | |||
| a493606f64 | |||
| f108cca5f5 | |||
| d93da15528 | |||
| 227d01ba5e | |||
| 58839933fc | |||
| d1af7958f8 | |||
| c314268d1e | |||
| 2ecbc9bfd4 | |||
| 0d6ea4264f | |||
| 961d2499de | |||
| cfd5483a18 | |||
| e329e5bb67 | |||
| 43cae0d03c | |||
| 9973a16ada | |||
| 0f13ba384e | |||
| c1b5b5fee2 | |||
| ba3e3b4a55 | |||
| 739759628a | |||
| e7afa96a0b | |||
| 62623b7123 | |||
| 82b3b526a7 | |||
| ce25ef38b0 | |||
| e21dd7e70f | |||
| 7439005ed7 | |||
| fb11622c32 | |||
| 1bc1d81fde | |||
| ae2c62350a | |||
| 9aebfd787b | |||
| 8b94837f18 | |||
| 5fe46cd6f5 | |||
| 81dcb2a92e | |||
| f5bad95cd5 | |||
| 7807f5bc90 | |||
| 5e55210eac | |||
| 6f78b88007 | |||
| cb1751ea4e | |||
| bfbfdf6658 | |||
| 32c92fedf2 |
+17
@@ -1,8 +1,25 @@
|
|||||||
.idea
|
.idea
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.log
|
||||||
.vs
|
.vs
|
||||||
*.DotSettings*
|
*.DotSettings*
|
||||||
*.user
|
*.user
|
||||||
log*.txt
|
log*.txt
|
||||||
secured-config
|
secured-config
|
||||||
|
build
|
||||||
|
venv
|
||||||
|
*.c
|
||||||
|
*.pyd
|
||||||
|
cython_debug*
|
||||||
|
dist-dlls
|
||||||
|
dist-azaion
|
||||||
|
Azaion*.exe
|
||||||
|
Azaion*.bin
|
||||||
|
|
||||||
|
azaion\.*\.big
|
||||||
|
_internal
|
||||||
|
dist
|
||||||
|
*.jpg
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+152
-33
@@ -3,10 +3,12 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF"
|
||||||
|
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
|
||||||
xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||||
xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
|
xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
|
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||||
Title="Azaion Annotator" Height="800" Width="1100"
|
Title="Azaion Annotator" Height="800" Width="1100"
|
||||||
WindowState="Maximized"
|
WindowState="Maximized"
|
||||||
>
|
>
|
||||||
@@ -48,15 +50,19 @@
|
|||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<local:GradientStyleSelector x:Key="GradientStyleSelector"/>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid Name="GlobalGrid"
|
<Grid Name="GlobalGrid"
|
||||||
ShowGridLines="False"
|
ShowGridLines="False"
|
||||||
Background="Black">
|
Background="Black">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*" Name="DetectionSection" />
|
||||||
<RowDefinition Height="28"></RowDefinition>
|
<RowDefinition Height="0" Name="GpsSplitterRow" />
|
||||||
<RowDefinition Height="32"></RowDefinition>
|
<RowDefinition Height="0" Name="GpsSectionRow"/>
|
||||||
|
<RowDefinition Height="28" Name="ProgressBarSection"/>
|
||||||
|
<RowDefinition Height="32" Name="ButtonsSection"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
@@ -70,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>
|
||||||
@@ -148,14 +155,22 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Name="LvFiles"
|
Name="LvFiles"
|
||||||
Background="Black"
|
Background="Black"
|
||||||
SelectedItem="{Binding Path=SelectedVideo}" Foreground="#FFA4AFCC"
|
SelectedItem="{Binding Path=SelectedVideo}"
|
||||||
|
Foreground="#FFDDDDDD"
|
||||||
>
|
>
|
||||||
<ListView.Resources>
|
<ListView.Resources>
|
||||||
<Style TargetType="{x:Type ListViewItem}">
|
<Style TargetType="{x:Type ListViewItem}">
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
|
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
|
||||||
<Setter Property="Background" Value="Gray"/>
|
<Setter Property="Background" Value="#FF505050"/>
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Foreground" Value=" DimGray" />
|
||||||
|
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="DimGray"></Setter>
|
||||||
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
<EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter>
|
<EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -163,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>
|
||||||
@@ -176,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
|
||||||
@@ -187,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"
|
||||||
@@ -196,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"
|
||||||
@@ -209,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"
|
||||||
@@ -219,9 +245,8 @@
|
|||||||
<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"
|
||||||
RowBackground="#252525"
|
|
||||||
Foreground="White"
|
Foreground="White"
|
||||||
RowHeaderWidth="0"
|
RowHeaderWidth="0"
|
||||||
Padding="2 0 0 0"
|
Padding="2 0 0 0"
|
||||||
@@ -230,7 +255,8 @@
|
|||||||
CellStyle="{DynamicResource DataGridCellStyle1}"
|
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserResizeRows="False"
|
CanUserResizeRows="False"
|
||||||
CanUserResizeColumns="False">
|
CanUserResizeColumns="False"
|
||||||
|
RowStyleSelector="{StaticResource GradientStyleSelector}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn
|
||||||
Width="60"
|
Width="60"
|
||||||
@@ -253,28 +279,30 @@
|
|||||||
<Setter Property="Background" Value="#252525"></Setter>
|
<Setter Property="Background" Value="#252525"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</DataGridTextColumn.HeaderStyle>
|
</DataGridTextColumn.HeaderStyle>
|
||||||
<DataGridTextColumn.CellStyle>
|
|
||||||
<Style TargetType="DataGridCell">
|
|
||||||
<Setter Property="Background">
|
|
||||||
<Setter.Value>
|
|
||||||
<LinearGradientBrush StartPoint="0 0 " EndPoint="1 0">
|
|
||||||
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor0}" />
|
|
||||||
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor1}" />
|
|
||||||
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor2}" />
|
|
||||||
<GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" />
|
|
||||||
</LinearGradientBrush>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
</DataGridTextColumn.CellStyle>
|
|
||||||
</DataGridTextColumn>
|
</DataGridTextColumn>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<GridSplitter
|
||||||
|
Name="GpsSplitter"
|
||||||
|
Background="DarkGray"
|
||||||
|
ResizeDirection="Rows"
|
||||||
|
Grid.Row="1"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
|
||||||
|
<controls:MapMatcher
|
||||||
|
x:Name="MapMatcherComponent"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="2"
|
||||||
|
/>
|
||||||
|
|
||||||
<controls2:UpdatableProgressBar x:Name="VideoSlider"
|
<controls2:UpdatableProgressBar x:Name="VideoSlider"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="1"
|
Grid.Row="3"
|
||||||
Background="#252525"
|
Background="#252525"
|
||||||
Foreground="LightBlue">
|
Foreground="LightBlue">
|
||||||
</controls2:UpdatableProgressBar>
|
</controls2:UpdatableProgressBar>
|
||||||
@@ -282,7 +310,7 @@
|
|||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<Grid
|
<Grid
|
||||||
Name="Buttons"
|
Name="Buttons"
|
||||||
Grid.Row="2"
|
Grid.Row="4"
|
||||||
Background="Black"
|
Background="Black"
|
||||||
>
|
>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
@@ -297,7 +325,10 @@
|
|||||||
<ColumnDefinition Width="28" /> <!-- 8 -->
|
<ColumnDefinition Width="28" /> <!-- 8 -->
|
||||||
<ColumnDefinition Width="56" /> <!-- 9 -->
|
<ColumnDefinition Width="56" /> <!-- 9 -->
|
||||||
<ColumnDefinition Width="28" /> <!-- 10 -->
|
<ColumnDefinition Width="28" /> <!-- 10 -->
|
||||||
<ColumnDefinition Width="*" /> <!-- 11 -->
|
<ColumnDefinition Width="28" /> <!-- 11 -->
|
||||||
|
<ColumnDefinition Width="28" /> <!-- 12 -->
|
||||||
|
<ColumnDefinition Width="28" /> <!-- 13 -->
|
||||||
|
<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"
|
||||||
Click="PlayClick">
|
Click="PlayClick">
|
||||||
@@ -477,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. Клавіша: [A]" 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
|
||||||
@@ -508,8 +540,95 @@
|
|||||||
21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053
|
21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053
|
||||||
8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" />
|
8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Grid.Column="11" Padding="2" Width="25" Height="25" ToolTip="Показати GPS. Клавіша: [M]" Background="Black" BorderBrush="Black"
|
||||||
|
Click="SwitchGpsPanel">
|
||||||
|
<Image>
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V520 H580 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="M307.1,311.97c-12.55-14-22.75-31.86-32.9-47.68c-10.23-15.94-19.78-32.43-27.3-49.83c-7.03-16.28-12.48-33.08-9.25-50.97
|
||||||
|
c2.87-15.93,11.75-31.29,23.84-42.03c22.3-19.8,57.81-22.55,82.67-5.98c29.17,19.45,39.48,55.06,27.59,87.55
|
||||||
|
c-6.8,18.59-16.41,36.14-27.02,52.8C332.76,274.63,320.84,294.45,307.1,311.97z M307.01,143.45c-38.65-0.46-39.68,59.79-0.95,60.47
|
||||||
|
c16.47,0.29,30.83-13.34,31-29.9C337.22,157.75,323.24,143.65,307.01,143.45z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="M367.34,310.68c10.09,2.5,23.61,4.83,31.46,12.19c11.05,10.35-5.42,18.17-14.21,21.43c-24.55,9.11-53.52,10.41-79.44,10.11
|
||||||
|
c-25.7-0.3-54.62-1.23-78.68-11.19c-7.68-3.18-21.53-10.2-12.52-19.47c8.26-8.49,23.33-11.42,34.5-12.94
|
||||||
|
c-5.15,1.98-16.18,5.12-17.07,11.49c-1,7.13,9.78,10.81,15.02,12.59c18.28,6.22,38.72,7.58,57.89,7.73
|
||||||
|
c18.91,0.15,38.85-0.72,57.13-5.92c5.72-1.63,18.65-4.74,20.7-11.49c2.28-7.47-9.8-11.66-15.04-13.71
|
||||||
|
C367.18,311.22,367.26,310.95,367.34,310.68z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="12"
|
||||||
|
Padding="2"
|
||||||
|
Width="25"
|
||||||
|
Height="25"
|
||||||
|
ToolTip="Показати обєкти по аудіоаналізу. Клавіша: [R]" Background="Black" BorderBrush="Black"
|
||||||
|
Click="SoundDetections">
|
||||||
|
<Image>
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup>
|
||||||
|
<GeometryDrawing Geometry="m19.05,171.43v152.38">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
<GeometryDrawing Geometry="m95.24,95.24v342.86">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
<GeometryDrawing Geometry="m171.43,209.52v76.19">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
<GeometryDrawing Geometry="m247.62,133.33v259.58">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
|
||||||
<StatusBar Grid.Column="11"
|
<GeometryDrawing Geometry="m323.81,19.05v457.14">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
<GeometryDrawing Geometry="m401.43,86.69v342.86">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
<GeometryDrawing Geometry="m473.43,209.02v76.19">
|
||||||
|
<GeometryDrawing.Pen>
|
||||||
|
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
|
||||||
|
</GeometryDrawing.Pen>
|
||||||
|
</GeometryDrawing>
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
</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"
|
||||||
Background="#252525"
|
Background="#252525"
|
||||||
Foreground="White">
|
Foreground="White">
|
||||||
<StatusBar.ItemsPanel>
|
<StatusBar.ItemsPanel>
|
||||||
|
|||||||
+314
-414
File diff suppressed because it is too large
Load Diff
@@ -1,12 +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.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.Events;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
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;
|
||||||
@@ -16,22 +25,32 @@ 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,
|
||||||
|
IOptions<AnnotationConfig> annotationConfig,
|
||||||
|
IInferenceService inferenceService,
|
||||||
|
IDbFactory dbFactory,
|
||||||
|
IAzaionApi api,
|
||||||
|
FailsafeAnnotationsProducer producer)
|
||||||
:
|
:
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<AnnClassSelectedEvent>,
|
INotificationHandler<AnnClassSelectedEvent>,
|
||||||
INotificationHandler<PlaybackControlEvent>,
|
INotificationHandler<AnnotatorControlEvent>,
|
||||||
INotificationHandler<VolumeChangedEvent>
|
INotificationHandler<VolumeChangedEvent>,
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
@@ -45,21 +64,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectClass(DetectionClass annClass)
|
private void SelectClass(DetectionClass detClass)
|
||||||
{
|
{
|
||||||
mainWindow.Editor.CurrentAnnClass = annClass;
|
mainWindow.Editor.CurrentAnnClass = detClass;
|
||||||
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
|
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
|
||||||
ann.DetectionClass = annClass;
|
ann.DetectionClass = detClass;
|
||||||
mainWindow.LvClasses.SelectedIndex = annClass.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;
|
||||||
@@ -72,22 +91,22 @@ public class AnnotatorEventHandler(
|
|||||||
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
|
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
|
||||||
keyNumber = key - Key.NumPad1;
|
keyNumber = key - Key.NumPad1;
|
||||||
if (keyNumber.HasValue)
|
if (keyNumber.HasValue)
|
||||||
SelectClass((DetectionClass)mainWindow.LvClasses.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.A)
|
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:
|
||||||
@@ -105,9 +124,9 @@ public class AnnotatorEventHandler(
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(PlaybackControlEvent 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,19 +140,29 @@ public class AnnotatorEventHandler(
|
|||||||
switch (controlEnum)
|
switch (controlEnum)
|
||||||
{
|
{
|
||||||
case PlaybackControlEnum.Play:
|
case PlaybackControlEnum.Play:
|
||||||
Play();
|
await Play(cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Pause:
|
case PlaybackControlEnum.Pause:
|
||||||
|
if (mediaPlayer.IsPlaying)
|
||||||
|
{
|
||||||
mediaPlayer.Pause();
|
mediaPlayer.Pause();
|
||||||
if (!mediaPlayer.IsPlaying)
|
mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
|
||||||
|
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mediaPlayer.Play();
|
||||||
if (formState.BackgroundTime.HasValue)
|
if (formState.BackgroundTime.HasValue)
|
||||||
{
|
{
|
||||||
mainWindow.Editor.ResetBackground();
|
mainWindow.Editor.SetBackground(null);
|
||||||
formState.BackgroundTime = null;
|
formState.BackgroundTime = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Stop:
|
case PlaybackControlEnum.Stop:
|
||||||
|
inferenceService.StopInference();
|
||||||
|
await mainWindow.DetCancelSource.CancelAsync();
|
||||||
mediaPlayer.Stop();
|
mediaPlayer.Stop();
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.PreviousFrame:
|
case PlaybackControlEnum.PreviousFrame:
|
||||||
@@ -143,10 +172,17 @@ 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);
|
||||||
|
if (focusedElement is ListViewItem item)
|
||||||
|
{
|
||||||
|
if (item.DataContext is not MediaFileInfo mediaFileInfo)
|
||||||
|
return;
|
||||||
|
mainWindow.DeleteMedia(mediaFileInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
mainWindow.Editor.RemoveSelectedAnns();
|
mainWindow.Editor.RemoveSelectedAnns();
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.RemoveAllAnns:
|
case PlaybackControlEnum.RemoveAllAnns:
|
||||||
@@ -164,10 +200,10 @@ public class AnnotatorEventHandler(
|
|||||||
mediaPlayer.Volume = 0;
|
mediaPlayer.Volume = 0;
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Previous:
|
case PlaybackControlEnum.Previous:
|
||||||
NextMedia(isPrevious: true);
|
await NextMedia(isPrevious: true, ct: cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Next:
|
case PlaybackControlEnum.Next:
|
||||||
NextMedia();
|
await NextMedia(ct: cancellationToken);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.None:
|
case PlaybackControlEnum.None:
|
||||||
break;
|
break;
|
||||||
@@ -182,7 +218,7 @@ public class AnnotatorEventHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NextMedia(bool isPrevious = false)
|
private async Task NextMedia(bool isPrevious = false, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var increment = isPrevious ? -1 : 1;
|
var increment = isPrevious ? -1 : 1;
|
||||||
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
|
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
|
||||||
@@ -190,10 +226,10 @@ public class AnnotatorEventHandler(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
mainWindow.LvFiles.SelectedIndex += increment;
|
mainWindow.LvFiles.SelectedIndex += increment;
|
||||||
Play();
|
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;
|
||||||
@@ -205,68 +241,259 @@ public class AnnotatorEventHandler(
|
|||||||
mediaPlayer.Volume = volume;
|
mediaPlayer.Volume = volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Play()
|
private async Task Play(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
|
||||||
|
|
||||||
|
await mainWindow.ReloadAnnotations();
|
||||||
|
|
||||||
|
if (mediaInfo.MediaType == MediaTypes.Video)
|
||||||
|
{
|
||||||
|
mainWindow.Editor.SetBackground(null);
|
||||||
|
//need to wait a bit for correct VLC playback event handling
|
||||||
|
await Task.Delay(100, ct);
|
||||||
mediaPlayer.Stop();
|
mediaPlayer.Stop();
|
||||||
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
|
mediaPlayer.Play(new Media(libVlc, mediaInfo.Path));
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
|
}
|
||||||
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 fName = formState.GetTimeName(time);
|
var timeName = formState.MediaName.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();
|
|
||||||
|
|
||||||
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken);
|
|
||||||
|
|
||||||
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
|
|
||||||
mainWindow.LvFiles.Items.Refresh();
|
|
||||||
mainWindow.Editor.RemoveAllAnns();
|
|
||||||
|
|
||||||
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
|
||||||
var imageExtension = isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path);
|
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
|
||||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}");
|
|
||||||
|
|
||||||
|
formState.CurrentMedia.HasAnnotations = mainWindow.Editor.CurrentDetections.Count != 0;
|
||||||
|
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
|
||||||
if (isVideo)
|
if (isVideo)
|
||||||
{
|
{
|
||||||
if (formState.BackgroundTime.HasValue)
|
foreach (var annotation in annotations)
|
||||||
{
|
mainWindow.AddAnnotation(annotation);
|
||||||
//no need to save image, it's already there, just remove background
|
|
||||||
mainWindow.Editor.ResetBackground();
|
|
||||||
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
|
|
||||||
{
|
|
||||||
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
|
|
||||||
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
|
|
||||||
mediaPlayer.Play();
|
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;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
|
await NextMedia(ct: cancellationToken);
|
||||||
NextMedia();
|
|
||||||
}
|
}
|
||||||
await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
|
|
||||||
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
|
mainWindow.Editor.RemoveAllAnns();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
|
||||||
|
|
||||||
|
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
|
||||||
|
var mediaSize = new Size(source.PixelWidth, source.PixelHeight);
|
||||||
|
var annotationsResult = new List<Annotation>();
|
||||||
|
if (!File.Exists(imgPath))
|
||||||
|
{
|
||||||
|
if (mediaSize.FitSizeForAI())
|
||||||
|
await source.SaveImage(imgPath, cancellationToken);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Tiling
|
||||||
|
|
||||||
|
//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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,27 +7,32 @@
|
|||||||
<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="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="8.0.0" />
|
<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.EF6" Version="1.0.119" />
|
||||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
|
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
|
||||||
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
||||||
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Effects;
|
||||||
|
using GMap.NET.WindowsPresentation;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls
|
||||||
|
{
|
||||||
|
public class CircleVisual : FrameworkElement
|
||||||
|
{
|
||||||
|
public readonly GMapMarker Marker;
|
||||||
|
|
||||||
|
public CircleVisual(GMapMarker m, int size, string text, Brush background)
|
||||||
|
{
|
||||||
|
ShadowEffect = new DropShadowEffect();
|
||||||
|
Marker = m;
|
||||||
|
Marker.ZIndex = 100;
|
||||||
|
|
||||||
|
SizeChanged += CircleVisual_SizeChanged;
|
||||||
|
MouseEnter += CircleVisual_MouseEnter;
|
||||||
|
MouseLeave += CircleVisual_MouseLeave;
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
|
||||||
|
Text = text;
|
||||||
|
|
||||||
|
StrokeArrow.EndLineCap = PenLineCap.Triangle;
|
||||||
|
StrokeArrow.LineJoin = PenLineJoin.Round;
|
||||||
|
|
||||||
|
RenderTransform = _scale;
|
||||||
|
|
||||||
|
Width = Height = size;
|
||||||
|
FontSize = Width / 1.55;
|
||||||
|
|
||||||
|
Background = background;
|
||||||
|
Angle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CircleVisual_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Marker.Offset = new Point(-e.NewSize.Width / 2, -e.NewSize.Height / 2);
|
||||||
|
_scale.CenterX = -Marker.Offset.X;
|
||||||
|
_scale.CenterY = -Marker.Offset.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateVisual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly ScaleTransform _scale = new ScaleTransform(1, 1);
|
||||||
|
|
||||||
|
void CircleVisual_MouseLeave(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
|
Marker.ZIndex -= 10000;
|
||||||
|
Cursor = Cursors.Arrow;
|
||||||
|
|
||||||
|
Effect = null;
|
||||||
|
|
||||||
|
_scale.ScaleY = 1;
|
||||||
|
_scale.ScaleX = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CircleVisual_MouseEnter(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
Marker.ZIndex += 10000;
|
||||||
|
Cursor = Cursors.Hand;
|
||||||
|
|
||||||
|
Effect = ShadowEffect;
|
||||||
|
|
||||||
|
_scale.ScaleY = 1.5;
|
||||||
|
_scale.ScaleX = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DropShadowEffect ShadowEffect;
|
||||||
|
|
||||||
|
static readonly Typeface Font = new Typeface(new FontFamily("Arial"),
|
||||||
|
FontStyles.Normal,
|
||||||
|
FontWeights.Bold,
|
||||||
|
FontStretches.Normal);
|
||||||
|
|
||||||
|
FormattedText _fText = null!;
|
||||||
|
|
||||||
|
private Brush _background = Brushes.Blue;
|
||||||
|
|
||||||
|
public Brush Background
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _background;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_background != value)
|
||||||
|
{
|
||||||
|
_background = value;
|
||||||
|
IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Brush _foreground = Brushes.White;
|
||||||
|
|
||||||
|
public Brush Foreground
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _foreground;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_foreground != value)
|
||||||
|
{
|
||||||
|
_foreground = value;
|
||||||
|
IsChanged = true;
|
||||||
|
|
||||||
|
ForceUpdateText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pen _stroke = new Pen(Brushes.Blue, 2.0);
|
||||||
|
|
||||||
|
public Pen Stroke
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _stroke;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_stroke != value)
|
||||||
|
{
|
||||||
|
_stroke = value;
|
||||||
|
IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pen _strokeArrow = new Pen(Brushes.Blue, 2.0);
|
||||||
|
|
||||||
|
public Pen StrokeArrow
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _strokeArrow;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_strokeArrow != value)
|
||||||
|
{
|
||||||
|
_strokeArrow = value;
|
||||||
|
IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double FontSize = 16;
|
||||||
|
|
||||||
|
private double? _angle = 0;
|
||||||
|
|
||||||
|
public double? Angle
|
||||||
|
{
|
||||||
|
get => _angle;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (!_angle.HasValue || !value.HasValue ||
|
||||||
|
Angle.HasValue && Math.Abs(_angle.Value - value.Value) > 11)
|
||||||
|
{
|
||||||
|
_angle = value;
|
||||||
|
IsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsChanged = true;
|
||||||
|
|
||||||
|
void ForceUpdateText()
|
||||||
|
{
|
||||||
|
|
||||||
|
_fText = new FormattedText(_text,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
FlowDirection.LeftToRight,
|
||||||
|
Font,
|
||||||
|
FontSize,
|
||||||
|
Foreground, 1.0);
|
||||||
|
IsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string _text = null!;
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _text;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_text != value)
|
||||||
|
{
|
||||||
|
_text = value;
|
||||||
|
ForceUpdateText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Visual _child = null!;
|
||||||
|
|
||||||
|
public virtual Visual? Child
|
||||||
|
{
|
||||||
|
get => _child;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_child == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_child != null)
|
||||||
|
{
|
||||||
|
RemoveLogicalChild(_child);
|
||||||
|
RemoveVisualChild(_child);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
AddVisualChild(value);
|
||||||
|
AddLogicalChild(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache the new child
|
||||||
|
_child = value!;
|
||||||
|
|
||||||
|
InvalidateVisual();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateVisual(bool forceUpdate)
|
||||||
|
{
|
||||||
|
if (forceUpdate || IsChanged)
|
||||||
|
{
|
||||||
|
Child = Create();
|
||||||
|
IsChanged = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _countCreate;
|
||||||
|
|
||||||
|
private DrawingVisual Create()
|
||||||
|
{
|
||||||
|
_countCreate++;
|
||||||
|
|
||||||
|
var square = new DrawingVisualFx();
|
||||||
|
|
||||||
|
using var dc = square.RenderOpen();
|
||||||
|
dc.DrawEllipse(null,
|
||||||
|
Stroke,
|
||||||
|
new Point(Width / 2, Height / 2),
|
||||||
|
Width / 2 + Stroke.Thickness / 2,
|
||||||
|
Height / 2 + Stroke.Thickness / 2);
|
||||||
|
|
||||||
|
if (Angle.HasValue)
|
||||||
|
{
|
||||||
|
dc.PushTransform(new RotateTransform(Angle.Value, Width / 2, Height / 2));
|
||||||
|
{
|
||||||
|
var polySeg = new PolyLineSegment(new[]
|
||||||
|
{
|
||||||
|
new Point(Width * 0.2, Height * 0.3), new Point(Width * 0.8, Height * 0.3)
|
||||||
|
},
|
||||||
|
true);
|
||||||
|
var pathFig = new PathFigure(new Point(Width * 0.5, -Height * 0.22),
|
||||||
|
new PathSegment[] {polySeg},
|
||||||
|
true);
|
||||||
|
var pathGeo = new PathGeometry(new[] {pathFig});
|
||||||
|
dc.DrawGeometry(Brushes.AliceBlue, StrokeArrow, pathGeo);
|
||||||
|
}
|
||||||
|
dc.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
dc.DrawEllipse(Background, null, new Point(Width / 2, Height / 2), Width / 2, Height / 2);
|
||||||
|
dc.DrawText(_fText, new Point(Width / 2 - _fText.Width / 2, Height / 2 - _fText.Height / 2));
|
||||||
|
|
||||||
|
return square;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Necessary Overrides -- Needed by WPF to maintain bookkeeping of our hosted visuals
|
||||||
|
|
||||||
|
protected override int VisualChildrenCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Child == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Visual? GetVisualChild(int index)
|
||||||
|
{
|
||||||
|
return Child;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DrawingVisualFx : DrawingVisual
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect",
|
||||||
|
typeof(Effect),
|
||||||
|
typeof(DrawingVisualFx),
|
||||||
|
new FrameworkPropertyMetadata(null,
|
||||||
|
FrameworkPropertyMetadataOptions.AffectsRender,
|
||||||
|
OnEffectChanged));
|
||||||
|
|
||||||
|
public new Effect Effect
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return (Effect)GetValue(EffectProperty);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetValue(EffectProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnEffectChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var drawingVisualFx = o as DrawingVisualFx;
|
||||||
|
if (drawingVisualFx != null)
|
||||||
|
{
|
||||||
|
drawingVisualFx.SetMyProtectedVisualEffect((Effect)e.NewValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMyProtectedVisualEffect(Effect effect)
|
||||||
|
{
|
||||||
|
VisualEffect = effect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<UserControl x:Class="Azaion.Annotator.Controls.MapMatcher"
|
||||||
|
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.Annotator.Controls"
|
||||||
|
xmlns:windowsPresentation="clr-namespace:GMap.NET.WindowsPresentation;assembly=GMap.NET.WindowsPresentation"
|
||||||
|
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="300" d:DesignWidth="1200">
|
||||||
|
<Grid
|
||||||
|
Name="MatcherGrid"
|
||||||
|
ShowGridLines="False"
|
||||||
|
Background="Black"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="250" /> <!-- 0 list view -->
|
||||||
|
<ColumnDefinition Width="4"/> <!-- 1 splitter -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- 2 ExplorerEditor -->
|
||||||
|
<ColumnDefinition Width="4"/> <!-- 3 splitter -->
|
||||||
|
<ColumnDefinition Width="*" /> <!-- 4 Maps Control -->
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Grid.Column="0">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="28"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="32"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Margin="1"
|
||||||
|
x:Name="TbGpsMapFolder"></TextBox>
|
||||||
|
<Button
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="1"
|
||||||
|
Click="OpenGpsTilesFolderClick">
|
||||||
|
. . .
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
Grid.Row="1"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="50"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<!-- <TextBlock -->
|
||||||
|
<!-- Grid.Column="0" -->
|
||||||
|
<!-- Text="Lat" -->
|
||||||
|
<!-- Background="Gray"/> -->
|
||||||
|
<Button
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="1"
|
||||||
|
Click="TestGps">
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
x:Name="TbLat"
|
||||||
|
Text="48.2748909"></TextBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
Grid.Row="2"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="50"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="Lon"
|
||||||
|
Background="Gray"/>
|
||||||
|
<TextBox
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
x:Name="TbLon"
|
||||||
|
Text="37.3834877"></TextBox>
|
||||||
|
</Grid>
|
||||||
|
<ListView Grid.Row="3"
|
||||||
|
Name="GpsFiles"
|
||||||
|
Background="Black"
|
||||||
|
SelectedItem="{Binding Path=SelectedVideo}"
|
||||||
|
Foreground="#FFDDDDDD">
|
||||||
|
<ListView.Resources>
|
||||||
|
<Style TargetType="{x:Type ListViewItem}">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
|
||||||
|
<Setter Property="Background" Value="#FF505050"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Foreground" Value=" DimGray" />
|
||||||
|
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="DimGray"></Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
<EventSetter Event="ContextMenuOpening" Handler="GpsFilesContextOpening"></EventSetter>
|
||||||
|
</Style>
|
||||||
|
</ListView.Resources>
|
||||||
|
<ListView.ContextMenu>
|
||||||
|
<ContextMenu Name="GpsFilesContextMenu">
|
||||||
|
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
|
||||||
|
</ContextMenu>
|
||||||
|
</ListView.ContextMenu>
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Width="Auto"
|
||||||
|
Header="Файл"
|
||||||
|
DisplayMemberBinding="{Binding Path=Name}"/>
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<GridSplitter
|
||||||
|
Background="DarkGray"
|
||||||
|
ResizeDirection="Columns"
|
||||||
|
Grid.Column="1"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"/>
|
||||||
|
|
||||||
|
<Border Grid.Column="2" ClipToBounds="True">
|
||||||
|
<controls:CanvasEditor
|
||||||
|
x:Name="GpsImageEditor"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<GridSplitter
|
||||||
|
Background="DarkGray"
|
||||||
|
ResizeDirection="Columns"
|
||||||
|
Grid.Column="3"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"/>
|
||||||
|
|
||||||
|
<windowsPresentation:GMapControl
|
||||||
|
Grid.Column="4"
|
||||||
|
x:Name="SatelliteMap"
|
||||||
|
Zoom="20" MaxZoom="24" MinZoom="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
MinWidth="400" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
using Azaion.Common.Services;
|
||||||
|
using GMap.NET;
|
||||||
|
using GMap.NET.MapProviders;
|
||||||
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls;
|
||||||
|
|
||||||
|
public partial class MapMatcher : UserControl
|
||||||
|
{
|
||||||
|
private AppConfig _appConfig = null!;
|
||||||
|
List<MediaFileInfo> _allMediaFiles = new();
|
||||||
|
public Dictionary<int, Annotation> Annotations = new();
|
||||||
|
private string _currentDir = null!;
|
||||||
|
private IGpsMatcherService _gpsMatcherService = null!;
|
||||||
|
|
||||||
|
public MapMatcher()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init(AppConfig appConfig, IGpsMatcherService gpsMatcherService)
|
||||||
|
{
|
||||||
|
_appConfig = appConfig;
|
||||||
|
_gpsMatcherService = gpsMatcherService;
|
||||||
|
GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey;
|
||||||
|
SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap;
|
||||||
|
SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501);
|
||||||
|
SatelliteMap.MultiTouchEnabled = true;
|
||||||
|
|
||||||
|
GpsFiles.MouseDoubleClick += async (sender, args) => { await OpenGpsLocation(GpsFiles.SelectedIndex); };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenGpsLocation(int gpsFilesIndex)
|
||||||
|
{
|
||||||
|
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
|
||||||
|
var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
|
||||||
|
if (ann == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
GpsImageEditor.Background = new ImageBrush
|
||||||
|
{
|
||||||
|
ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage()
|
||||||
|
};
|
||||||
|
if (ann.Lat != 0 && ann.Lon != 0)
|
||||||
|
SatelliteMap.Position = new PointLatLng(ann.Lat, ann.Lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GpsFilesContextOpening(object sender, ContextMenuEventArgs e)
|
||||||
|
{
|
||||||
|
var listItem = sender as ListViewItem;
|
||||||
|
GpsFilesContextMenu.DataContext = listItem!.DataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenContainingFolder(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo;
|
||||||
|
if (mediaFileInfo == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OpenGpsTilesFolderClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new CommonOpenFileDialog
|
||||||
|
{
|
||||||
|
Title = "Open Video folder",
|
||||||
|
IsFolderPicker = true,
|
||||||
|
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
|
||||||
|
};
|
||||||
|
var dialogResult = dlg.ShowDialog();
|
||||||
|
|
||||||
|
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
TbGpsMapFolder.Text = dlg.FileName;
|
||||||
|
_currentDir = dlg.FileName;
|
||||||
|
var dir = new DirectoryInfo(dlg.FileName);
|
||||||
|
var mediaFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
|
||||||
|
.Select(x => new MediaFileInfo
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
Path = x.FullName,
|
||||||
|
MediaType = MediaTypes.Image
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_allMediaFiles = mediaFiles;
|
||||||
|
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
|
||||||
|
|
||||||
|
Annotations = mediaFiles.Select((x, i) => (i, new Annotation
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
OriginalMediaName = x.Name
|
||||||
|
})).ToDictionary(x => x.i, x => x.Item2);
|
||||||
|
|
||||||
|
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||||
|
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TestGps(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
|
||||||
|
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
using System.Windows.Input;
|
using MediatR;
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator.DTO;
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
public class PlaybackControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
|
|
||||||
{
|
|
||||||
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class VolumeChangedEvent(int volume) : INotification
|
public class VolumeChangedEvent(int volume) : INotification
|
||||||
{
|
{
|
||||||
public int Volume { get; set; } = volume;
|
public int Volume { get; set; } = volume;
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator.Extensions;
|
|
||||||
|
|
||||||
public static class PointExtensions
|
|
||||||
{
|
|
||||||
public static double SqrDistance(this Point p1, Point p2) =>
|
|
||||||
(p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y);
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using LibVLCSharp.Shared;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator.Extensions;
|
|
||||||
|
|
||||||
public class VLCFrameExtractor(LibVLC libVLC, IOptions<AIRecognitionConfig> config)
|
|
||||||
{
|
|
||||||
private const uint RGBA_BYTES = 4;
|
|
||||||
private const int PLAYBACK_RATE = 4;
|
|
||||||
|
|
||||||
private uint _pitch; // Number of bytes per "line", aligned to x32.
|
|
||||||
private uint _lines; // Number of lines in the buffer, aligned to x32.
|
|
||||||
private uint _width; // Thumbnail width
|
|
||||||
private uint _height; // Thumbnail height
|
|
||||||
|
|
||||||
private MediaPlayer _mediaPlayer = null!;
|
|
||||||
|
|
||||||
private TimeSpan _lastFrameTimestamp;
|
|
||||||
private long _lastFrame;
|
|
||||||
|
|
||||||
private static uint Align32(uint size)
|
|
||||||
{
|
|
||||||
if (size % 32 == 0)
|
|
||||||
return size;
|
|
||||||
return (size / 32 + 1) * 32;// Align on the next multiple of 32
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SKBitmap? _currentBitmap;
|
|
||||||
private static readonly ConcurrentQueue<FrameInfo> FramesQueue = new();
|
|
||||||
private static long _frameCounter;
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<(TimeSpan Time, Stream Stream)> ExtractFrames(string mediaPath,
|
|
||||||
[EnumeratorCancellation] CancellationToken manualCancellationToken = default)
|
|
||||||
{
|
|
||||||
var videoFinishedCancellationSource = new CancellationTokenSource();
|
|
||||||
|
|
||||||
_mediaPlayer = new MediaPlayer(libVLC);
|
|
||||||
_mediaPlayer.Stopped += (s, e) => videoFinishedCancellationSource.CancelAfter(1);
|
|
||||||
|
|
||||||
using var media = new Media(libVLC, mediaPath);
|
|
||||||
await media.Parse(cancellationToken: videoFinishedCancellationSource.Token);
|
|
||||||
var videoTrack = media.Tracks.FirstOrDefault(x => x.Data.Video.Width != 0);
|
|
||||||
_width = videoTrack.Data.Video.Width;
|
|
||||||
_height = videoTrack.Data.Video.Height;
|
|
||||||
|
|
||||||
_pitch = Align32(_width * RGBA_BYTES);
|
|
||||||
_lines = Align32(_height);
|
|
||||||
_mediaPlayer.SetRate(PLAYBACK_RATE);
|
|
||||||
|
|
||||||
media.AddOption(":no-audio");
|
|
||||||
_mediaPlayer.SetVideoFormat("RV32", _width, _height, _pitch);
|
|
||||||
_mediaPlayer.SetVideoCallbacks(Lock, null, Display);
|
|
||||||
|
|
||||||
_mediaPlayer.Play(media);
|
|
||||||
_frameCounter = 0;
|
|
||||||
var surface = SKSurface.Create(new SKImageInfo((int) _width, (int) _height));
|
|
||||||
var videoFinishedCT = videoFinishedCancellationSource.Token;
|
|
||||||
|
|
||||||
while ( !(FramesQueue.IsEmpty && videoFinishedCT.IsCancellationRequested || manualCancellationToken.IsCancellationRequested))
|
|
||||||
{
|
|
||||||
if (FramesQueue.TryDequeue(out var frameInfo))
|
|
||||||
{
|
|
||||||
if (frameInfo.Bitmap == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
surface.Canvas.DrawBitmap(frameInfo.Bitmap, 0, 0); // Effectively crops the original bitmap to get only the visible area
|
|
||||||
|
|
||||||
using var outputImage = surface.Snapshot();
|
|
||||||
using var data = outputImage.Encode(SKEncodedImageFormat.Jpeg, 85);
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
data.SaveTo(ms);
|
|
||||||
|
|
||||||
yield return (frameInfo.Time, ms);
|
|
||||||
|
|
||||||
frameInfo.Bitmap?.Dispose();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(1), videoFinishedCT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FramesQueue.Clear(); //clear queue in case of manual stop
|
|
||||||
_mediaPlayer.Stop();
|
|
||||||
_mediaPlayer.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr Lock(IntPtr opaque, IntPtr planes)
|
|
||||||
{
|
|
||||||
_currentBitmap = new SKBitmap(new SKImageInfo((int)(_pitch / RGBA_BYTES), (int)_lines, SKColorType.Bgra8888));
|
|
||||||
Marshal.WriteIntPtr(planes, _currentBitmap.GetPixels());
|
|
||||||
return IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Display(IntPtr opaque, IntPtr picture)
|
|
||||||
{
|
|
||||||
var playerTime = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
|
||||||
if (_lastFrameTimestamp != playerTime)
|
|
||||||
{
|
|
||||||
_lastFrame = _frameCounter;
|
|
||||||
_lastFrameTimestamp = playerTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_frameCounter > 20 && _frameCounter % config.Value.FramePeriodRecognition == 0)
|
|
||||||
{
|
|
||||||
var msToAdd = (_frameCounter - _lastFrame) * (_lastFrame == 0 ? 0 : _lastFrameTimestamp.TotalMilliseconds / _lastFrame);
|
|
||||||
var time = _lastFrameTimestamp.Add(TimeSpan.FromMilliseconds(msToAdd));
|
|
||||||
|
|
||||||
FramesQueue.Enqueue(new FrameInfo(time, _currentBitmap));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentBitmap?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentBitmap = null;
|
|
||||||
_frameCounter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FrameInfo(TimeSpan time, SKBitmap? bitmap)
|
|
||||||
{
|
|
||||||
public TimeSpan Time { get; set; } = time;
|
|
||||||
public SKBitmap? Bitmap { get; set; } = bitmap;
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using Azaion.Annotator.DTO;
|
|
||||||
using Azaion.Annotator.Extensions;
|
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.Common.Services;
|
|
||||||
using Azaion.CommonSecurity.Services;
|
|
||||||
using Compunet.YoloV8;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using Detection = Azaion.Common.DTO.Detection;
|
|
||||||
|
|
||||||
namespace Azaion.Annotator;
|
|
||||||
|
|
||||||
public interface IAIDetector
|
|
||||||
{
|
|
||||||
Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable
|
|
||||||
{
|
|
||||||
private readonly AIRecognitionConfig _recognitionConfig = recognitionConfig.Value;
|
|
||||||
private YoloPredictor? _predictor;
|
|
||||||
private const string YOLO_MODEL = "azaion.onnx";
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_predictor == null)
|
|
||||||
{
|
|
||||||
await using var stream = await resourceLoader.Load(YOLO_MODEL, cancellationToken);
|
|
||||||
_predictor = new YoloPredictor(stream.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
imageStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
var image = Image.Load<Rgb24>(imageStream);
|
|
||||||
var result = await _predictor.DetectAsync(image);
|
|
||||||
|
|
||||||
var imageSize = new System.Windows.Size(image.Width, image.Height);
|
|
||||||
|
|
||||||
var detections = result.Select(d =>
|
|
||||||
{
|
|
||||||
var label = new YoloLabel(new CanvasLabel(d.Name.Id, d.Bounds.X, d.Bounds.Y, d.Bounds.Width, d.Bounds.Height), imageSize, imageSize);
|
|
||||||
return new Detection(fName, label, (double?)d.Confidence * 100);
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
return FilterOverlapping(detections);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Detection> FilterOverlapping(List<Detection> detections)
|
|
||||||
{
|
|
||||||
var k = _recognitionConfig.TrackingIntersectionThreshold;
|
|
||||||
var filteredDetections = new List<Detection>();
|
|
||||||
for (var i = 0; i < detections.Count; i++)
|
|
||||||
{
|
|
||||||
var detectionSelected = false;
|
|
||||||
for (var j = i + 1; j < detections.Count; j++)
|
|
||||||
{
|
|
||||||
var intersect = detections[i].ToRectangle();
|
|
||||||
intersect.Intersect(detections[j].ToRectangle());
|
|
||||||
|
|
||||||
var maxArea = Math.Max(detections[i].ToRectangle().Area(), detections[j].ToRectangle().Area());
|
|
||||||
if (!(intersect.Area() > k * maxArea))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (detections[i].Probability > detections[j].Probability)
|
|
||||||
{
|
|
||||||
filteredDetections.Add(detections[i]);
|
|
||||||
detections.RemoveAt(j);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
filteredDetections.Add(detections[j]);
|
|
||||||
detections.RemoveAt(i);
|
|
||||||
}
|
|
||||||
detectionSelected = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detectionSelected)
|
|
||||||
filteredDetections.Add(detections[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredDetections;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() => _predictor?.Dispose();
|
|
||||||
}
|
|
||||||
@@ -4,22 +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="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.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||||
|
<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="RabbitMQ.Stream.Client" Version="1.8.9" />
|
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
</ItemGroup>
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||||
|
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
|
||||||
<ItemGroup>
|
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||||
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
|
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+226
-56
@@ -1,97 +1,186 @@
|
|||||||
using System.Windows;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
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 SECURE_RESOURCE_CACHE = "SecureResourceCache";
|
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";
|
||||||
|
private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
||||||
|
private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region AnnotatorConfig
|
#region AnnotatorConfig
|
||||||
|
|
||||||
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
||||||
[
|
[
|
||||||
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
|
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 },
|
||||||
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
|
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 },
|
||||||
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" },
|
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 },
|
||||||
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" },
|
new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 },
|
||||||
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" },
|
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 },
|
||||||
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" },
|
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 },
|
||||||
new() { Id = 6, Name = "Військовий", ShortName = "Військов" },
|
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 },
|
||||||
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
|
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 },
|
||||||
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
|
new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 },
|
||||||
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
|
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 },
|
||||||
new() { Id = 10, Name = "Літак", ShortName = "Літак" },
|
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 },
|
||||||
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" }
|
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"];
|
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi", "ts"];
|
||||||
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
||||||
|
|
||||||
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
private static readonly AnnotationConfig DefaultAnnotationConfig = new()
|
||||||
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
{
|
||||||
|
DetectionClasses = DefaultAnnotationClasses,
|
||||||
|
VideoFormats = DefaultVideoFormats,
|
||||||
|
ImageFormats = DefaultImageFormats,
|
||||||
|
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
|
||||||
|
};
|
||||||
|
|
||||||
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
#region UIConfig
|
||||||
|
public const int DEFAULT_LEFT_PANEL_WIDTH = 200;
|
||||||
|
public const int DEFAULT_RIGHT_PANEL_WIDTH = 200;
|
||||||
|
#endregion UIConfig
|
||||||
|
|
||||||
|
#region CameraConfig
|
||||||
|
|
||||||
|
public const int DEFAULT_ALTITUDE = 400;
|
||||||
|
public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m;
|
||||||
|
public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m;
|
||||||
|
|
||||||
|
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 const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
|
||||||
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
{
|
||||||
public const double TRACKING_PROBABILITY_INCREASE = 15;
|
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
|
||||||
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
|
||||||
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
|
||||||
|
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
|
||||||
|
BigImageTileOverlapPercent = DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT,
|
||||||
|
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
|
||||||
|
};
|
||||||
|
|
||||||
|
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
||||||
|
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
||||||
|
private const double TRACKING_PROBABILITY_INCREASE = 15;
|
||||||
|
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
||||||
|
private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
|
||||||
|
private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
||||||
|
|
||||||
# endregion AIRecognitionConfig
|
# endregion AIRecognitionConfig
|
||||||
|
|
||||||
|
# region GpsDeniedConfig
|
||||||
|
|
||||||
|
private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
|
||||||
|
{
|
||||||
|
MinKeyPoints = 11
|
||||||
|
};
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
#region Thumbnails
|
#region Thumbnails
|
||||||
|
|
||||||
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
private static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||||
|
|
||||||
public const int DEFAULT_THUMBNAIL_BORDER = 10;
|
private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
|
||||||
|
{
|
||||||
|
Size = DefaultThumbnailSize,
|
||||||
|
Border = DEFAULT_THUMBNAIL_BORDER
|
||||||
|
};
|
||||||
|
|
||||||
|
private const int DEFAULT_THUMBNAIL_BORDER = 10;
|
||||||
|
|
||||||
public const string THUMBNAIL_PREFIX = "_thumb";
|
public const string THUMBNAIL_PREFIX = "_thumb";
|
||||||
|
public const string RESULT_PREFIX = "_result";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public static TimeSpan? GetTime(string imagePath)
|
|
||||||
{
|
|
||||||
var timeStr = imagePath.Split("_").LastOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
|
|
||||||
if (!int.TryParse(timeStr[0..1], out var hours))
|
|
||||||
return null;
|
|
||||||
if (!int.TryParse(timeStr[1..3], out var minutes))
|
|
||||||
return null;
|
|
||||||
if (!int.TryParse(timeStr[3..5], out var seconds))
|
|
||||||
return null;
|
|
||||||
if (!int.TryParse(timeStr[5..6], out var milliseconds))
|
|
||||||
return null;
|
|
||||||
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Queue
|
|
||||||
|
|
||||||
public const string MQ_DIRECT_TYPE = "direct";
|
|
||||||
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
||||||
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
|
|
||||||
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
|
|
||||||
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Database
|
#region Database
|
||||||
|
|
||||||
@@ -99,7 +188,88 @@ 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
|
||||||
|
|
||||||
|
#region Mode Captions
|
||||||
|
|
||||||
|
public const string REGULAR_MODE_CAPTION = "Норма";
|
||||||
|
public const string WINTER_MODE_CAPTION = "Зима";
|
||||||
|
public const string NIGHT_MODE_CAPTION = "Ніч";
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -26,21 +32,26 @@ 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 MatrixTransform _matrixTransform = new();
|
||||||
|
private Point _panStartPoint;
|
||||||
|
private bool _isZoomedIn;
|
||||||
|
|
||||||
|
private const int MIN_SIZE = 12;
|
||||||
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
|
||||||
|
|
||||||
public IMediator Mediator { get; set; } = null!;
|
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),
|
||||||
typeof(Func<TimeSpan?>),
|
typeof(Func<TimeSpan>),
|
||||||
typeof(CanvasEditor),
|
typeof(CanvasEditor),
|
||||||
new PropertyMetadata(null));
|
new PropertyMetadata(null));
|
||||||
|
|
||||||
public Func<TimeSpan?> GetTimeFunc
|
public Func<TimeSpan> GetTimeFunc
|
||||||
{
|
{
|
||||||
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp);
|
get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
|
||||||
set => SetValue(GetTimeFuncProp, value);
|
set => SetValue(GetTimeFuncProp, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +65,7 @@ public class CanvasEditor : Canvas
|
|||||||
_verticalLine.Fill = value.ColorBrush;
|
_verticalLine.Fill = value.ColorBrush;
|
||||||
_horizontalLine.Stroke = value.ColorBrush;
|
_horizontalLine.Stroke = value.ColorBrush;
|
||||||
_horizontalLine.Fill = value.ColorBrush;
|
_horizontalLine.Fill = value.ColorBrush;
|
||||||
_classNameHint.Text = value.Name;
|
_classNameHint.Text = value.ShortName;
|
||||||
|
|
||||||
_newAnnotationRect.Stroke = value.ColorBrush;
|
_newAnnotationRect.Stroke = value.ColorBrush;
|
||||||
_newAnnotationRect.Fill = value.ColorBrush;
|
_newAnnotationRect.Fill = value.ColorBrush;
|
||||||
@@ -84,7 +95,7 @@ public class CanvasEditor : Canvas
|
|||||||
};
|
};
|
||||||
_classNameHint = new TextBlock
|
_classNameHint = new TextBlock
|
||||||
{
|
{
|
||||||
Text = CurrentAnnClass?.Name ?? "asd",
|
Text = CurrentAnnClass?.ShortName ?? "",
|
||||||
Foreground = new SolidColorBrush(Colors.Black),
|
Foreground = new SolidColorBrush(Colors.Black),
|
||||||
Cursor = Cursors.Arrow,
|
Cursor = Cursors.Arrow,
|
||||||
FontSize = 16,
|
FontSize = 16,
|
||||||
@@ -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,42 +178,165 @@ public class CanvasEditor : Canvas
|
|||||||
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
ClearSelections();
|
ClearSelections();
|
||||||
|
if (e.LeftButton != MouseButtonState.Pressed)
|
||||||
|
return;
|
||||||
|
if (Keyboard.Modifiers == ModifierKeys.Control && _isZoomedIn)
|
||||||
|
{
|
||||||
|
_panStartPoint = e.GetPosition(this);
|
||||||
|
SelectionState = SelectionState.PanZoomMoving;
|
||||||
|
}
|
||||||
|
else
|
||||||
NewAnnotationStart(sender, e);
|
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)
|
switch (SelectionState)
|
||||||
return;
|
{
|
||||||
if (SelectionState == SelectionState.NewAnnCreating)
|
case SelectionState.NewAnnCreating:
|
||||||
NewAnnotationCreatingMove(sender, e);
|
NewAnnotationCreatingMove(pos);
|
||||||
|
break;
|
||||||
|
case SelectionState.AnnResizing:
|
||||||
|
AnnotationResizeMove(pos);
|
||||||
|
break;
|
||||||
|
case SelectionState.AnnMoving:
|
||||||
|
AnnotationPositionMove(pos);
|
||||||
|
e.Handled = true;
|
||||||
|
break;
|
||||||
|
case SelectionState.PanZoomMoving:
|
||||||
|
PanZoomMove(pos);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (SelectionState == SelectionState.AnnResizing)
|
private Point GetClampedPosition(MouseEventArgs e)
|
||||||
AnnotationResizeMove(sender, 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (SelectionState == SelectionState.AnnMoving)
|
private void PanZoomMove(Point point)
|
||||||
AnnotationPositionMove(sender, e);
|
{
|
||||||
|
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)
|
||||||
CreateAnnotation(e.GetPosition(this));
|
{
|
||||||
|
var endPos = GetClampedPosition(e);
|
||||||
|
_newAnnotationRect.Width = 0;
|
||||||
|
_newAnnotationRect.Height = 0;
|
||||||
|
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
||||||
|
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
||||||
|
if (width >= MIN_SIZE && height >= MIN_SIZE)
|
||||||
|
{
|
||||||
|
var time = GetTimeFunc();
|
||||||
|
var control = CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
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
|
||||||
@@ -174,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):
|
||||||
@@ -227,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)
|
||||||
@@ -244,19 +416,26 @@ public class CanvasEditor : Canvas
|
|||||||
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;
|
|
||||||
|
|
||||||
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX);
|
var nextLeft = GetLeft(_curAnn) + offsetX;
|
||||||
SetTop(_curAnn, GetTop(_curAnn) + offsetY);
|
var nextTop = GetTop(_curAnn) + offsetY;
|
||||||
_lastPos = currentPos;
|
|
||||||
e.Handled = true;
|
if (_clampedRect.HasValue)
|
||||||
|
{
|
||||||
|
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
|
||||||
@@ -266,64 +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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateAnnotation(Point endPos)
|
public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
|
||||||
{
|
{
|
||||||
_newAnnotationRect.Width = 0;
|
foreach (var detection in annotation.Detections)
|
||||||
_newAnnotationRect.Height = 0;
|
{
|
||||||
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
|
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
|
||||||
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
|
CanvasLabel canvasLabel;
|
||||||
if (width < MIN_SIZE || height < MIN_SIZE)
|
if (!annotation.IsSplit || mediaSize.FitSizeForAI())
|
||||||
return;
|
canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
canvasLabel = new CanvasLabel(detection, annotation.SplitTile!.Size, null, detection.Confidence)
|
||||||
|
.ReframeFromSmall(annotation.SplitTile);
|
||||||
|
|
||||||
var time = GetTimeFunc();
|
//From CurrentMediaSize to Render Size
|
||||||
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
|
var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
|
||||||
{
|
canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
|
||||||
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
|
var control = CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
|
||||||
|
control.UpdateLayout();
|
||||||
|
CheckLabelBoundaries(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
|
||||||
{
|
{
|
||||||
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability)
|
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
|
||||||
{
|
detectionControl.MouseDown += AnnotationPositionStart;
|
||||||
Width = canvasLabel.Width,
|
SetLeft(detectionControl, canvasLabel.Left );
|
||||||
Height = canvasLabel.Height
|
SetTop(detectionControl, canvasLabel.Top);
|
||||||
};
|
Children.Add(detectionControl);
|
||||||
annotationControl.MouseDown += AnnotationPositionStart;
|
CurrentDetections.Add(detectionControl);
|
||||||
SetLeft(annotationControl, canvasLabel.X );
|
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
|
||||||
SetTop(annotationControl, canvasLabel.Y);
|
return detectionControl;
|
||||||
Children.Add(annotationControl);
|
|
||||||
CurrentDetections.Add(annotationControl);
|
|
||||||
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
|
|
||||||
return annotationControl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -355,11 +537,16 @@ public class CanvasEditor : Canvas
|
|||||||
public void ClearExpiredAnnotations(TimeSpan time)
|
public void ClearExpiredAnnotations(TimeSpan time)
|
||||||
{
|
{
|
||||||
var expiredAnns = CurrentDetections.Where(x =>
|
var expiredAnns = CurrentDetections.Where(x =>
|
||||||
x.Time.HasValue &&
|
Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
|
||||||
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
|
|
||||||
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,44 @@
|
|||||||
<DataGrid x:Class="Azaion.Common.Controls.DetectionClasses"
|
<UserControl x:Class="Azaion.Common.Controls.DetectionClasses"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="300" d:DesignWidth="300"
|
d:DesignHeight="300" d:DesignWidth="300">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Style x:Key="ButtonRadioButtonStyle" TargetType="RadioButton">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
Background="{TemplateBinding Background}" BorderThickness="1"
|
||||||
|
Padding="10,5" CornerRadius="2">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Border" Property="Background" Value="Gray"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Border" Property="Background" Value="DarkGray"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="White"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
<Grid Background="Black">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Your DataGrid with detection classes -->
|
||||||
|
<DataGrid x:Name="DetectionDataGrid"
|
||||||
|
Grid.Row="0"
|
||||||
Background="Black"
|
Background="Black"
|
||||||
RowBackground="#252525"
|
RowBackground="#252525"
|
||||||
Foreground="White"
|
Foreground="White"
|
||||||
@@ -15,35 +49,136 @@
|
|||||||
CellStyle="{DynamicResource DataGridCellStyle1}"
|
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserResizeRows="False"
|
CanUserResizeRows="False"
|
||||||
CanUserResizeColumns="False">
|
CanUserResizeColumns="False"
|
||||||
|
SelectionChanged="DetectionDataGrid_SelectionChanged"
|
||||||
|
x:FieldModifier="public"
|
||||||
|
PreviewKeyDown="OnKeyBanActivity"
|
||||||
|
>
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTemplateColumn
|
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
|
||||||
Width="50"
|
|
||||||
Header="Клавіша"
|
|
||||||
CanUserSort="False">
|
|
||||||
<DataGridTemplateColumn.HeaderStyle>
|
<DataGridTemplateColumn.HeaderStyle>
|
||||||
<Style TargetType="DataGridColumnHeader">
|
<Style TargetType="DataGridColumnHeader">
|
||||||
<Setter Property="Background" Value="#252525"></Setter>
|
<Setter Property="Background" Value="#252525"/>
|
||||||
</Style>
|
</Style>
|
||||||
</DataGridTemplateColumn.HeaderStyle>
|
</DataGridTemplateColumn.HeaderStyle>
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Border Background="{Binding Path=ColorBrush}">
|
<Border Background="{Binding Path=ColorBrush}">
|
||||||
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
|
<TextBlock Text="{Binding Path=ClassNumber}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn Width="*" Header="Назва" Binding="{Binding Path=ShortName}" CanUserSort="False">
|
||||||
Width="*"
|
|
||||||
Header="Назва"
|
|
||||||
Binding="{Binding Path=Name}"
|
|
||||||
CanUserSort="False">
|
|
||||||
<DataGridTextColumn.HeaderStyle>
|
<DataGridTextColumn.HeaderStyle>
|
||||||
<Style TargetType="DataGridColumnHeader">
|
<Style TargetType="DataGridColumnHeader">
|
||||||
<Setter Property="Background" Value="#252525"></Setter>
|
<Setter Property="Background" Value="#252525"/>
|
||||||
</Style>
|
</Style>
|
||||||
</DataGridTextColumn.HeaderStyle>
|
</DataGridTextColumn.HeaderStyle>
|
||||||
</DataGridTextColumn>
|
</DataGridTextColumn>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
<!-- StackPanel with mode switcher RadioButtons -->
|
||||||
|
<StackPanel x:Name="ModeSwitcherPanel"
|
||||||
|
Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Margin="0,2,0,2">
|
||||||
|
<RadioButton x:Name="NormalModeRadioButton"
|
||||||
|
Tag="0"
|
||||||
|
GroupName="Mode"
|
||||||
|
Checked="ModeRadioButton_Checked"
|
||||||
|
IsChecked="True"
|
||||||
|
Style="{StaticResource ButtonRadioButtonStyle}">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image Height="16" Width="16">
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m256,105.5c-83.9,0-152.2,68.3-152.2,152.2 0,83.9 68.3,152.2 152.2,152.2 83.9,0 152.2-68.3
|
||||||
|
152.2-152.2 0-84-68.3-152.2-152.2-152.2zm0,263.5c-61.4,0-111.4-50-111.4-111.4 0-61.4 50-111.4 111.4-111.4 61.4,0 111.4,50 111.4,111.4
|
||||||
|
0,61.4-50,111.4-111.4,111.4z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m256,74.8c11.3,0 20.4-9.1 20.4-20.4v-23c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v23c2.84217e-14,11.3 9.1,20.4 20.4,20.4z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m256,437.2c-11.3,0-20.4,9.1-20.4,20.4v22.9c0,11.3 9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-22.9c0-11.2-9.1-20.4-20.4-20.4z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m480.6,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h23c11.3,0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m54.4,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h22.9c11.3,0 20.4-9.1 20.4-20.4 0.1-11.3-9.1-20.4-20.3-20.4z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="M400.4,82.8L384.1,99c-8,8-8,20.9,0,28.9s20.9,8,28.9,0l16.2-16.2c8-8,8-20.9,0-28.9S408.3,74.8,400.4,82.8z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m99,384.1l-16.2,16.2c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l16.2-16.2c8-8 8-20.9 0-28.9s-20.9-7.9-28.9,0z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m413,384.1c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m99,127.9c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Name="RegularModeButton"
|
||||||
|
Padding="3"
|
||||||
|
/>
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
<RadioButton x:Name="EveningModeRadioButton"
|
||||||
|
Tag="20"
|
||||||
|
GroupName="Mode"
|
||||||
|
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
|
||||||
|
Style="{StaticResource ButtonRadioButtonStyle}">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image Height="16" Width="16">
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m444.8,256l50.2-50.2c8-8 8-20.9 0-28.9-8-8-20.9-8-28.9,0l-58.7,58.7h-85c-1.3-4.2-3-8.3-5-12.1l60.1-60.1h83c11.3,
|
||||||
|
0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-71v-71c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4v83l-60.1,60.1c-3.8-2-7.9-3.7-12.1-5v-85l58.7-58.7c8-8 8-20.9
|
||||||
|
0-28.9-8-8-20.9-8-28.9,0l-50.3,50.1-50.2-50.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l58.7,58.7v85c-4.2,1.3-8.3,
|
||||||
|
3-12.1,5l-60.1-60.1v-83c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v71h-71c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,
|
||||||
|
20.4h83l60.1,60.1c-2,3.8-3.7,7.9-5,12.1h-85l-58.7-58.7c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l50.1,50.3-50.2,50.2c-8,8-8,20.9 0,28.9
|
||||||
|
8,8 20.9,8 28.9,0l58.7-58.7h85c1.3,4.2 3,8.3 5,12.1l-60.1,60.1h-83c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h71v71c0,11.3
|
||||||
|
9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-83l60.1-60.1c3.8,2 7.9,3.7 12.1,5v85l-58.7,58.7c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l50.2-50.2
|
||||||
|
50.2,50.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-58.7-58.7v-85c4.2-1.3 8.3-3 12.1-5l60.1,60.1v83c0,11.3 9.1,20.4 20.4,20.4s20.4-9.1 20.4-20.4v-71h71c11.3,0
|
||||||
|
20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-83l-60.1-60.1c2-3.8 3.7-7.9 5-12.1h85l58.7,58.7c8,8 20.9,8 28.9,0 8-8 8-20.9
|
||||||
|
0-28.9l-50-50.2zm-217.3,0c0-15.7 12.8-28.5 28.5-28.5s28.5,12.8 28.5,28.5-12.8,28.5-28.5,28.5-28.5-12.8-28.5-28.5z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Name="WinterModeButton"
|
||||||
|
Padding="3"
|
||||||
|
/>
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
<RadioButton x:Name="NightModeRadioButton"
|
||||||
|
Tag="40"
|
||||||
|
GroupName="Mode"
|
||||||
|
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
|
||||||
|
Style="{StaticResource ButtonRadioButtonStyle}"
|
||||||
|
>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image Height="16" Width="16">
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m500,113.1c-2.4-7.5-8.9-13-16.8-14.1l-55.2-7.9-24.6-48.9c-3.5-7-10.7-11.4-18.5-11.4-7.8,0-15,4.4-18.5,11.4l-24.6,
|
||||||
|
48.9-55.2,7.9c-7.8,1.1-14.3,6.6-16.8,14.1-2.4,7.5-0.3,15.8 5.4,21.3l39.7,37.9-9.4,53.4c-1.4,7.7 1.8,15.6 8.1,20.2 6.3,4.7 14.7,5.3 21.7,1.7l49.5-25.5
|
||||||
|
49.5,25.5c3,1.5 6.2,2.3 9.5,2.3 4.3,0 8.6-1.4 12.2-4 6.3-4.6 9.5-12.5 8.1-20.2l-9.4-53.4 39.7-37.9c5.9-5.5 8-13.8 5.6-21.3zm-81.6,36.9c-5,4.8-7.3,
|
||||||
|
11.7-6.1,18.5l4.1,23.3-22-11.3c-5.9-3-13-3-18.9,0l-22,11.3 4.1-23.3c1.2-6.8-1.1-13.7-6.1-18.5l-16.9-16.2 23.8-3.4c6.7-1 12.5-5.1 15.5-11.2l11-21.9
|
||||||
|
11,21.9c3,6 8.8,10.2 15.5,11.2l23.8,3.4-16.8,16.2z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m442,361c-14.9,3.4-30.3,5.1-45.7,5.1-113.8,0-206.4-92.6-206.4-206.3 0-41.8 12.4-82 35.9-116.3
|
||||||
|
4.8-7 4.8-16.3 0-23.4-4.8-7.1-13.4-10.5-21.8-8.6-54,12.2-103,42.7-138,86-35.4,43.8-55,99.2-55,155.7 0,66.2 25.8,128.4 72.6,175.2 46.8,46.8
|
||||||
|
109.1,72.6 175.3,72.6 81.9,0 158.4-40.4 204.8-108.1 4.8-7 4.8-16.3 0-23.4-4.8-7-13.4-10.4-21.7-8.5zm-183.1,98.5c-113.8,0-206.4-92.6-206.4-206.3
|
||||||
|
0-78.2 45.3-149.1 112.8-183.8-11.2,28.6-17,59.1-17, 90.4 0,66.2 25.8,128.4 72.6,175.2 46.7,46.7 108.8,72.5 174.9,72.6-37.3,33.1-85.8,51.9-136.9,51.9z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Name="NightModeButton"
|
||||||
|
Padding="3"
|
||||||
|
/>
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
|
|||||||
@@ -1,9 +1,97 @@
|
|||||||
namespace Azaion.Common.Controls;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Controls;
|
||||||
|
|
||||||
|
public class DetectionClassChangedEventArgs(DetectionClass detectionClass, int classNumber) : EventArgs
|
||||||
|
{
|
||||||
|
public DetectionClass DetectionClass { get; } = detectionClass;
|
||||||
|
public int ClassNumber { get; } = classNumber;
|
||||||
|
}
|
||||||
|
|
||||||
public partial class DetectionClasses
|
public partial class DetectionClasses
|
||||||
{
|
{
|
||||||
|
public event EventHandler<DetectionClassChangedEventArgs>? DetectionClassChanged;
|
||||||
|
private const int CaptionedMinWidth = 230;
|
||||||
|
ObservableCollection<DetectionClass> _detectionClasses = new();
|
||||||
|
|
||||||
public DetectionClasses()
|
public DetectionClasses()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
SizeChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.NewSize.Width < CaptionedMinWidth)
|
||||||
|
{
|
||||||
|
RegularModeButton.Text = "";
|
||||||
|
WinterModeButton.Text = "";
|
||||||
|
NightModeButton.Text = "";
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RegularModeButton.Text = Constants.REGULAR_MODE_CAPTION;
|
||||||
|
WinterModeButton.Text= Constants.WINTER_MODE_CAPTION;
|
||||||
|
NightModeButton.Text= Constants.NIGHT_MODE_CAPTION;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init(List<DetectionClass> detectionClasses)
|
||||||
|
{
|
||||||
|
foreach (var dClass in detectionClasses)
|
||||||
|
{
|
||||||
|
var cl = (DetectionClass)dClass.Clone();
|
||||||
|
cl.Color = cl.Color.ToConfidenceColor();
|
||||||
|
_detectionClasses.Add(cl);
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectionDataGrid.ItemsSource = _detectionClasses;
|
||||||
|
DetectionDataGrid.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CurrentClassNumber { get; private set; } = 0;
|
||||||
|
public DetectionClass? CurrentDetectionClass { get; set; }
|
||||||
|
|
||||||
|
private void DetectionDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
|
||||||
|
RaiseDetectionClassChanged();
|
||||||
|
|
||||||
|
private void ModeRadioButton_Checked(object sender, RoutedEventArgs e) =>
|
||||||
|
RaiseDetectionClassChanged();
|
||||||
|
|
||||||
|
private void RaiseDetectionClassChanged()
|
||||||
|
{
|
||||||
|
var detClass = (DetectionClass)DetectionDataGrid.SelectedItem;
|
||||||
|
if (detClass == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var modeAmplifier = 0;
|
||||||
|
foreach (var child in ModeSwitcherPanel.Children)
|
||||||
|
if (child is RadioButton { IsChecked: true } rb)
|
||||||
|
if (int.TryParse(rb.Tag?.ToString(), out int modeIndex))
|
||||||
|
{
|
||||||
|
detClass.PhotoMode = (PhotoMode)modeIndex;
|
||||||
|
modeAmplifier += modeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentDetectionClass = detClass;
|
||||||
|
CurrentClassNumber = detClass.Id + modeAmplifier;
|
||||||
|
|
||||||
|
DetectionClassChanged?.Invoke(this, new DetectionClassChangedEventArgs(detClass, CurrentClassNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void SelectNum(int 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,22 @@ using System.Windows.Input;
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Label = System.Windows.Controls.Label;
|
using Azaion.Common.Extensions;
|
||||||
|
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 = 9;
|
private const double RESIZE_RECT_SIZE = 10;
|
||||||
|
|
||||||
private readonly Grid _grid;
|
private readonly Grid _grid;
|
||||||
private readonly TextBlock _classNameLabel;
|
private readonly DetectionLabelPanel _detectionLabelPanel;
|
||||||
private readonly Label _probabilityLabel;
|
public readonly Canvas DetectionLabelContainer;
|
||||||
public TimeSpan? Time { get; set; }
|
|
||||||
|
public TimeSpan Time { get; set; }
|
||||||
|
private readonly List<Rectangle> _resizedRectangles = new();
|
||||||
|
|
||||||
private DetectionClass _detectionClass = null!;
|
private DetectionClass _detectionClass = null!;
|
||||||
public DetectionClass DetectionClass
|
public DetectionClass DetectionClass
|
||||||
@@ -24,9 +27,13 @@ public class DetectionControl : Border
|
|||||||
get => _detectionClass;
|
get => _detectionClass;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_grid.Background = value.ColorBrush;
|
var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
|
||||||
_probabilityLabel.Background = value.ColorBrush;
|
BorderBrush = brush;
|
||||||
_classNameLabel.Text = value.Name;
|
BorderThickness = new Thickness(1);
|
||||||
|
foreach (var rect in _resizedRectangles)
|
||||||
|
rect.Stroke = brush;
|
||||||
|
|
||||||
|
_detectionLabelPanel.DetectionClass = value;
|
||||||
_detectionClass = value;
|
_detectionClass = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,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;
|
||||||
@@ -44,83 +52,114 @@ public class DetectionControl : Border
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
|
public void UpdateAdornerScale(double scale)
|
||||||
{
|
{
|
||||||
|
if (Math.Abs(scale) < 0.0001)
|
||||||
|
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;
|
||||||
|
Height = canvasLabel.Height;
|
||||||
Time = time;
|
Time = time;
|
||||||
_resizeStart = resizeStart;
|
_resizeStart = resizeStart;
|
||||||
_classNameLabel = new TextBlock
|
|
||||||
|
DetectionLabelContainer = new Canvas
|
||||||
{
|
{
|
||||||
Text = detectionClass.Name,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
|
||||||
Margin = new Thickness(0, 15, 0, 0),
|
|
||||||
FontSize = 14,
|
|
||||||
Cursor = Cursors.SizeAll
|
|
||||||
};
|
|
||||||
_probabilityLabel = new Label
|
|
||||||
{
|
|
||||||
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
Margin = new Thickness(0, -32, 0, 0),
|
ClipToBounds = false,
|
||||||
FontSize = 16,
|
|
||||||
Visibility = Visibility.Visible
|
|
||||||
};
|
};
|
||||||
|
_detectionLabelPanel = new DetectionLabelPanel
|
||||||
|
{
|
||||||
|
Confidence = canvasLabel.Confidence,
|
||||||
|
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
StrokeThickness = 2,
|
StrokeThickness = 2,
|
||||||
Visibility = Visibility.Collapsed
|
Visibility = Visibility.Collapsed
|
||||||
};
|
};
|
||||||
|
_resizedRectangles =
|
||||||
|
[
|
||||||
|
CreateResizeRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
|
||||||
|
CreateResizeRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
|
||||||
|
CreateResizeRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
|
||||||
|
CreateResizeRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
|
||||||
|
CreateResizeRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
|
||||||
|
CreateResizeRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
|
||||||
|
CreateResizeRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
|
||||||
|
CreateResizeRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
|
||||||
|
];
|
||||||
_grid = new Grid
|
_grid = new Grid
|
||||||
{
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
VerticalAlignment = VerticalAlignment.Stretch,
|
VerticalAlignment = VerticalAlignment.Stretch,
|
||||||
Children =
|
Children = { _selectionFrame }
|
||||||
{
|
|
||||||
_selectionFrame,
|
|
||||||
_classNameLabel,
|
|
||||||
AddRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
|
|
||||||
AddRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
|
|
||||||
AddRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
|
|
||||||
AddRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
|
|
||||||
AddRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
|
|
||||||
AddRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
|
|
||||||
AddRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
|
|
||||||
AddRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if (probability.HasValue)
|
_grid.Children.Add(DetectionLabelContainer);
|
||||||
_grid.Children.Add(_probabilityLabel);
|
foreach (var rect in _resizedRectangles)
|
||||||
|
_grid.Children.Add(rect);
|
||||||
|
|
||||||
Child = _grid;
|
Child = _grid;
|
||||||
Cursor = Cursors.SizeAll;
|
Cursor = Cursors.SizeAll;
|
||||||
DetectionClass = detectionClass;
|
DetectionClass = detectionClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
//small corners
|
//small corners
|
||||||
private Rectangle AddRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
|
private Rectangle CreateResizeRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
|
||||||
{
|
{
|
||||||
var rect = new Rectangle() // small rectangles at the corners and sides
|
var rect = new Rectangle() // small rectangles at the corners and sides
|
||||||
{
|
{
|
||||||
|
ClipToBounds = false,
|
||||||
|
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, 40, 40, 40)), // small rectangles color
|
Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
|
||||||
Fill = new SolidColorBrush(Color.FromArgb(1, 255, 255, 255)),
|
StrokeThickness = 0.8,
|
||||||
|
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.Id, 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
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.Common.DTO.Queue;
|
|
||||||
using Azaion.Common.Extensions;
|
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
|
||||||
|
|
||||||
public class Annotation
|
|
||||||
{
|
|
||||||
private static string _labelsDir = null!;
|
|
||||||
private static string _imagesDir = null!;
|
|
||||||
private static string _thumbDir = null!;
|
|
||||||
|
|
||||||
public static void InitializeDirs(DirectoriesConfig config)
|
|
||||||
{
|
|
||||||
_labelsDir = config.LabelsDirectory;
|
|
||||||
_imagesDir = config.ImagesDirectory;
|
|
||||||
_thumbDir = config.ThumbnailsDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
public string ImageExtension { get; set; } = null!;
|
|
||||||
public DateTime CreatedDate { get; set; }
|
|
||||||
public string CreatedEmail { get; set; } = null!;
|
|
||||||
public RoleEnum CreatedRole { get; set; }
|
|
||||||
public SourceEnum Source { get; set; }
|
|
||||||
public AnnotationStatus AnnotationStatus { get; set; }
|
|
||||||
|
|
||||||
public IEnumerable<Detection> Detections { get; set; } = null!;
|
|
||||||
|
|
||||||
public double Lat { get; set; }
|
|
||||||
public double Lon { get; set; }
|
|
||||||
|
|
||||||
public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
|
||||||
public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
|
||||||
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
|
||||||
public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum AnnotationStatus
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
Created = 10,
|
|
||||||
Validated = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AnnotationName
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = null!;
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,35 @@
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Newtonsoft.Json;
|
using Azaion.Common.Database;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class AnnotationResult
|
// public class AnnotationResult
|
||||||
{
|
//{
|
||||||
[JsonProperty(PropertyName = "f")]
|
//public Annotation Annotation { get; set; }
|
||||||
public string Image { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "t")]
|
|
||||||
public TimeSpan Time { get; set; }
|
|
||||||
|
|
||||||
public double Lat { get; set; }
|
//public string ImagePath { get; set; }
|
||||||
public double Lon { get; set; }
|
//public string TimeStr { get; set; }
|
||||||
public List<Detection> Detections { get; set; } = new();
|
|
||||||
|
|
||||||
#region For XAML Form
|
//public List<(Color Color, double Confidence)> Colors { get; private set; }
|
||||||
|
// public string ClassName { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
// public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
|
||||||
public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
// {
|
||||||
|
|
||||||
[JsonIgnore]
|
//Annotation = annotation;
|
||||||
public string ClassName { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
//TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
|
||||||
public Color ClassColor0 { get; set; }
|
//ImagePath = annotation.ImagePath;
|
||||||
|
|
||||||
[JsonIgnore]
|
// var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||||
public Color ClassColor1 { get; set; }
|
// ClassName = detectionClasses.Count > 1
|
||||||
|
// ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
|
||||||
|
// : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
||||||
|
//
|
||||||
|
// Colors = annotation.Detections
|
||||||
|
// .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
|
||||||
|
// .ToList();
|
||||||
|
|
||||||
[JsonIgnore]
|
// }
|
||||||
public Color ClassColor2 { get; set; }
|
// }
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Color ClassColor3 { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
}
|
|
||||||
+16
-9
@@ -2,13 +2,15 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.Extensions;
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class AnnotationImageView(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
|
||||||
@@ -19,20 +21,25 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
|
|||||||
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
|
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
|
||||||
return _thumbnail;
|
return _thumbnail;
|
||||||
}
|
}
|
||||||
private set => _thumbnail = value;
|
private set
|
||||||
}
|
|
||||||
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
|
||||||
|
|
||||||
public void Delete()
|
|
||||||
{
|
{
|
||||||
File.Delete(Annotation.ImagePath);
|
_thumbnail = value;
|
||||||
File.Delete(Annotation.LabelPath);
|
OnPropertyChanged();
|
||||||
File.Delete(Annotation.ThumbPath);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
||||||
|
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
|
||||||
|
public string CreatedEmail => Annotation.CreatedEmail;
|
||||||
|
public bool IsSeed => IsValidator &&
|
||||||
|
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
|
||||||
|
!Annotation.CreatedRole.IsValidator();
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateUI() => OnPropertyChanged(nameof(IsSeed));
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,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!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class BusinessExceptionDto
|
||||||
|
{
|
||||||
|
public int ErrorCode { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class ClusterDistribution
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public Color Color { get; set; }
|
||||||
|
public int ClassCount { get; set; }
|
||||||
|
public double BarWidth { get; set; }
|
||||||
|
}
|
||||||
@@ -1,10 +1,25 @@
|
|||||||
|
using MessagePack;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
public class AIRecognitionConfig
|
public class AIRecognitionConfig
|
||||||
{
|
{
|
||||||
public double FrameRecognitionSeconds { get; set; }
|
[Key("f_pr")] public int FramePeriodRecognition { get; set; }
|
||||||
public double TrackingDistanceConfidence { get; set; }
|
[Key("f_rs")] public double FrameRecognitionSeconds { get; set; }
|
||||||
public double TrackingProbabilityIncrease { get; set; }
|
[Key("pt")] public double ProbabilityThreshold { get; set; }
|
||||||
public double TrackingIntersectionThreshold { get; set; }
|
|
||||||
public int FramePeriodRecognition { get; set; }
|
[Key("t_dc")] public double TrackingDistanceConfidence { get; set; }
|
||||||
|
[Key("t_pi")] public double TrackingProbabilityIncrease { get; set; }
|
||||||
|
[Key("t_it")] public double TrackingIntersectionThreshold { get; set; }
|
||||||
|
|
||||||
|
[Key("d")] public byte[] Data { get; set; } = null!;
|
||||||
|
[Key("p")] public List<string> Paths { get; set; } = null!;
|
||||||
|
[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; }
|
||||||
}
|
}
|
||||||
@@ -4,20 +4,36 @@ namespace Azaion.Common.DTO.Config;
|
|||||||
|
|
||||||
public class AnnotationConfig
|
public class AnnotationConfig
|
||||||
{
|
{
|
||||||
public List<DetectionClass> AnnotationClasses { get; set; } = null!;
|
public List<DetectionClass> DetectionClasses { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
private Dictionary<int, DetectionClass>? _detectionClassesDict;
|
private Dictionary<int, DetectionClass>? _detectionClassesDict;
|
||||||
[JsonIgnore]
|
|
||||||
public Dictionary<int, DetectionClass> DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
|
|
||||||
|
|
||||||
public int? LastSelectedExplorerClass { get; set; }
|
[JsonIgnore]
|
||||||
|
public Dictionary<int, DetectionClass> DetectionClassesDict
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_detectionClassesDict != null)
|
||||||
|
return _detectionClassesDict;
|
||||||
|
|
||||||
|
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
|
||||||
|
|
||||||
|
_detectionClassesDict = DetectionClasses.SelectMany(cls => photoModes.Select(mode => new DetectionClass
|
||||||
|
{
|
||||||
|
Id = cls.Id,
|
||||||
|
Name = cls.Name,
|
||||||
|
Color = cls.Color,
|
||||||
|
ShortName = cls.ShortName,
|
||||||
|
PhotoMode = mode
|
||||||
|
}))
|
||||||
|
.ToDictionary(x => x.YoloId, x => x);
|
||||||
|
return _detectionClassesDict;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<string> VideoFormats { get; set; } = null!;
|
public List<string> VideoFormats { get; set; } = null!;
|
||||||
public List<string> ImageFormats { get; set; } = null!;
|
public List<string> ImageFormats { get; set; } = null!;
|
||||||
|
|
||||||
public string AnnotationsDbFile { get; set; } = null!;
|
public string AnnotationsDbFile { get; set; } = null!;
|
||||||
|
|
||||||
public double LeftPanelWidth { get; set; }
|
|
||||||
public double RightPanelWidth { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
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 ApiConfig ApiConfig { get; set; } = null!;
|
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
public QueueConfig QueueConfig { get; set; } = null!;
|
public QueueConfig QueueConfig { get; set; } = null!;
|
||||||
|
|
||||||
@@ -16,9 +19,17 @@ public class AppConfig
|
|||||||
|
|
||||||
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public UIConfig UIConfig { get; set; } = null!;
|
||||||
|
|
||||||
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
||||||
|
|
||||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public MapConfig MapConfig{ get; set; } = null!;
|
||||||
|
|
||||||
|
public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public CameraConfig CameraConfig { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConfigUpdater
|
public interface IConfigUpdater
|
||||||
@@ -29,64 +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);
|
||||||
{
|
|
||||||
ApiConfig = new ApiConfig
|
|
||||||
{
|
|
||||||
Url = SecurityConstants.DEFAULT_API_URL,
|
|
||||||
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
|
|
||||||
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
|
||||||
},
|
|
||||||
|
|
||||||
AnnotationConfig = new AnnotationConfig
|
|
||||||
{
|
|
||||||
AnnotationClasses = Constants.DefaultAnnotationClasses,
|
|
||||||
VideoFormats = Constants.DefaultVideoFormats,
|
|
||||||
ImageFormats = Constants.DefaultImageFormats,
|
|
||||||
|
|
||||||
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
|
||||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
|
||||||
|
|
||||||
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
|
|
||||||
},
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
|
|
||||||
ThumbnailConfig = new ThumbnailConfig
|
|
||||||
{
|
|
||||||
Size = Constants.DefaultThumbnailSize,
|
|
||||||
Border = Constants.DEFAULT_THUMBNAIL_BORDER
|
|
||||||
},
|
|
||||||
|
|
||||||
AIRecognitionConfig = new AIRecognitionConfig
|
|
||||||
{
|
|
||||||
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
|
|
||||||
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
|
|
||||||
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
|
|
||||||
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD,
|
|
||||||
FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Save(appConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(AppConfig config)
|
public void Save(AppConfig config)
|
||||||
{
|
{
|
||||||
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
var publicConfig = new
|
||||||
|
{
|
||||||
|
config.LoaderClientConfig,
|
||||||
|
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));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
public class MapConfig
|
||||||
|
{
|
||||||
|
public string Service { get; set; } = null!;
|
||||||
|
public string ApiKey { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
public class UIConfig
|
||||||
|
{
|
||||||
|
public double LeftPanelWidth { get; set; }
|
||||||
|
public double RightPanelWidth { get; set; }
|
||||||
|
public bool GenerateAnnotatedImage { 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);
|
||||||
|
}
|
||||||
@@ -1,22 +1,63 @@
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Azaion.Common.Extensions;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class DetectionClass
|
public class DetectionClass : ICloneable
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; } = null!;
|
public string Name { get; set; } = null!;
|
||||||
public string ShortName { get; set; } = null!;
|
public string ShortName { get; set; } = null!;
|
||||||
|
|
||||||
[JsonIgnore]
|
public Color Color { get; set; }
|
||||||
public Color Color => Id.ToColor();
|
|
||||||
|
public int MaxSizeM { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
public string UIName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var mode = PhotoMode switch
|
||||||
|
{
|
||||||
|
PhotoMode.Night => "(ніч)",
|
||||||
|
PhotoMode.Winter => "(зим)",
|
||||||
|
PhotoMode.Regular => "",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
return ShortName + mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public PhotoMode PhotoMode { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore] //For UI
|
||||||
public int ClassNumber => Id + 1;
|
public int ClassNumber => Id + 1;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public int YoloId => Id == -1 ? Id : (int)PhotoMode + Id;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public SolidColorBrush ColorBrush => new(Color);
|
public SolidColorBrush ColorBrush => new(Color);
|
||||||
|
|
||||||
|
public static DetectionClass FromYoloId(int yoloId, List<DetectionClass> detectionClasses)
|
||||||
|
{
|
||||||
|
var cls = yoloId % 20;
|
||||||
|
var photoMode = (PhotoMode)(yoloId - cls);
|
||||||
|
var detClass = detectionClasses[cls];
|
||||||
|
detClass.PhotoMode = photoMode;
|
||||||
|
return detClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Clone() => MemberwiseClone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PhotoMode
|
||||||
|
{
|
||||||
|
Regular = 0,
|
||||||
|
Winter = 20,
|
||||||
|
Night = 40
|
||||||
}
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
+6
-1
@@ -1,10 +1,15 @@
|
|||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class DirectoriesConfig
|
public class DirectoriesConfig
|
||||||
{
|
{
|
||||||
|
public string? ApiResourcesDirectory { get; set; } = null!;
|
||||||
|
|
||||||
public string VideosDirectory { get; set; } = null!;
|
public string VideosDirectory { get; set; } = null!;
|
||||||
public string LabelsDirectory { get; set; } = null!;
|
public string LabelsDirectory { get; set; } = null!;
|
||||||
public string ImagesDirectory { get; set; } = null!;
|
public string ImagesDirectory { get; set; } = null!;
|
||||||
public string ResultsDirectory { get; set; } = null!;
|
public string ResultsDirectory { get; set; } = null!;
|
||||||
public string ThumbnailsDirectory { get; set; } = null!;
|
public string ThumbnailsDirectory { get; set; } = null!;
|
||||||
|
|
||||||
|
public string GpsSatDirectory { get; set; } = null!;
|
||||||
|
public string GpsRouteDirectory { get; set; } = null!;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class DownloadTilesResult
|
||||||
|
{
|
||||||
|
public ConcurrentDictionary<(int x, int y), byte[]> Tiles { get; set; } = null!;
|
||||||
|
public double LatMin { get; set; }
|
||||||
|
public double LatMax { get; set; }
|
||||||
|
public double LonMin { get; set; }
|
||||||
|
public double LonMax { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public abstract class ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ZeroMqHost { get; set; } = "";
|
||||||
|
public int ZeroMqPort { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoaderClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ApiUrl { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ApiUrl { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsDeniedClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public int ZeroMqReceiverPort { get; set; }
|
||||||
|
}
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
|
||||||
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 => string.IsNullOrEmpty(CurrentMedia?.Name)
|
public string MediaName => CurrentMedia?.FName ?? "";
|
||||||
? ""
|
|
||||||
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
|
|
||||||
|
|
||||||
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; }
|
||||||
|
|
||||||
public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public static class EnumerableExtensions
|
public static class EnumerableExtensions
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class InitConfig
|
||||||
|
{
|
||||||
|
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
|
||||||
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
|
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||||
|
public CameraConfig CameraConfig { get; set; } = null!;
|
||||||
|
}
|
||||||
+67
-52
@@ -1,18 +1,18 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using MessagePack;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Size = System.Windows.Size;
|
using Size = System.Windows.Size;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
public abstract class Label
|
public abstract class Label
|
||||||
{
|
{
|
||||||
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; }
|
[JsonProperty(PropertyName = "cl")][Key("c")] public int ClassNumber { get; set; }
|
||||||
|
|
||||||
protected Label()
|
protected Label() { }
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Label(int classNumber)
|
protected Label(int classNumber)
|
||||||
{
|
{
|
||||||
@@ -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? Probability { get; }
|
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? probability = null) : 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;
|
||||||
Probability = probability;
|
Confidence = confidence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double? probability = null)
|
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,24 +94,32 @@ 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;
|
||||||
}
|
}
|
||||||
Probability = probability;
|
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]
|
||||||
public class YoloLabel : Label
|
public class YoloLabel : Label
|
||||||
{
|
{
|
||||||
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; }
|
[JsonProperty(PropertyName = "x")][Key("x")] public double CenterX { get; set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; }
|
[JsonProperty(PropertyName = "y")][Key("y")] public double CenterY { get; set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "w")] public double Width { get; set; }
|
[JsonProperty(PropertyName = "w")][Key("w")] public double Width { get; set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "h")] public double Height { get; set; }
|
[JsonProperty(PropertyName = "h")][Key("h")] public double Height { get; set; }
|
||||||
|
|
||||||
public YoloLabel()
|
public YoloLabel()
|
||||||
{
|
{
|
||||||
@@ -104,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;
|
||||||
@@ -118,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,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
|
||||||
{
|
{
|
||||||
@@ -183,23 +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(',', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Detection : YoloLabel
|
|
||||||
{
|
|
||||||
public string AnnotationName { get; set; }
|
|
||||||
public double? Probability { get; set; }
|
|
||||||
|
|
||||||
//For db
|
|
||||||
public Detection(){}
|
|
||||||
|
|
||||||
public Detection(string annotationName, YoloLabel label, double? probability = null)
|
|
||||||
{
|
|
||||||
AnnotationName = annotationName;
|
|
||||||
ClassNumber = label.ClassNumber;
|
|
||||||
CenterX = label.CenterX;
|
|
||||||
CenterY = label.CenterY;
|
|
||||||
Height = label.Height;
|
|
||||||
Width = label.Width;
|
|
||||||
Probability = probability;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class LoginResponse
|
public class LoginResponse
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Azaion.Common.DTO;
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public class MediaFileInfo
|
public class MediaFileInfo
|
||||||
{
|
{
|
||||||
@@ -8,4 +10,6 @@ public class MediaFileInfo
|
|||||||
public string DurationStr => $"{Duration:h\\:mm\\:ss}";
|
public string DurationStr => $"{Duration:h\\:mm\\:ss}";
|
||||||
public bool HasAnnotations { get; set; }
|
public bool HasAnnotations { get; set; }
|
||||||
public MediaTypes MediaType { get; set; }
|
public MediaTypes MediaType { get; set; }
|
||||||
|
|
||||||
|
public string FName => Name.ToFName();
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,6 @@ public enum PlaybackControlEnum
|
|||||||
TurnOnVolume = 10,
|
TurnOnVolume = 10,
|
||||||
Previous = 11,
|
Previous = 11,
|
||||||
Next = 12,
|
Next = 12,
|
||||||
Close = 13
|
Close = 13,
|
||||||
|
ValidateAnnotations = 15
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Database;
|
||||||
|
|
||||||
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 ImageExtension { get; set; } = null!;
|
[Key(2)] public string OriginalMediaName { get; set; } = null!;
|
||||||
[Key(3)] public string Detections { get; set; } = null!;
|
[Key(3)] public TimeSpan Time { get; set; }
|
||||||
[Key(4)] public byte[] Image { get; set; } = null!;
|
[Key(4)] public string ImageExtension { get; set; } = null!;
|
||||||
[Key(5)] public RoleEnum CreatedRole { get; set; }
|
[Key(5)] public string Detections { get; set; } = null!;
|
||||||
[Key(6)] public string CreatedEmail { get; set; } = null!;
|
[Key(6)] public byte[]? Image { get; set; } = null!;
|
||||||
[Key(7)] public SourceEnum Source { get; set; }
|
[Key(7)] public RoleEnum Role { get; set; }
|
||||||
[Key(8)] public AnnotationStatus Status { get; set; }
|
[Key(8)] public string Email { get; set; } = null!;
|
||||||
|
[Key(9)] public SourceEnum Source { 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,4 +1,4 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
public enum RoleEnum
|
public enum RoleEnum
|
||||||
{
|
{
|
||||||
@@ -7,6 +7,11 @@ public enum RoleEnum
|
|||||||
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
|
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
|
||||||
CompanionPC = 30,
|
CompanionPC = 30,
|
||||||
Admin = 40, //
|
Admin = 40, //
|
||||||
ResourceUploader = 50, //Uploading dll and ai models
|
|
||||||
ApiAdmin = 1000 //everything
|
ApiAdmin = 1000 //everything
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class RoleEnumExtensions
|
||||||
|
{
|
||||||
|
public static bool IsValidator(this RoleEnum role) =>
|
||||||
|
role.In(RoleEnum.Validator, RoleEnum.Admin, RoleEnum.ApiAdmin);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class SatTile
|
||||||
|
{
|
||||||
|
public int X { get; }
|
||||||
|
public int Y { get; }
|
||||||
|
public GeoPoint LeftTop { get; }
|
||||||
|
public GeoPoint BottomRight { get; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public SatTile(int x, int y, int zoom, string url)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
Url = url;
|
||||||
|
|
||||||
|
LeftTop = GeoUtils.TileToWorldPos(x, y, zoom);
|
||||||
|
BottomRight = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public RoleEnum Role { get; set; }
|
||||||
|
public UserConfig? UserConfig { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserConfig
|
||||||
|
{
|
||||||
|
public UserQueueOffsets? QueueOffsets { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserQueueOffsets
|
||||||
|
{
|
||||||
|
public ulong AnnotationsOffset { get; set; }
|
||||||
|
public ulong AnnotationsConfirmOffset { get; set; }
|
||||||
|
public ulong AnnotationsCommandsOffset { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class Annotation
|
||||||
|
{
|
||||||
|
private static string _labelsDir = null!;
|
||||||
|
private static string _imagesDir = null!;
|
||||||
|
private static string _thumbDir = null!;
|
||||||
|
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
|
||||||
|
|
||||||
|
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
|
||||||
|
{
|
||||||
|
_labelsDir = config.LabelsDirectory;
|
||||||
|
_imagesDir = config.ImagesDirectory;
|
||||||
|
_thumbDir = config.ThumbnailsDirectory;
|
||||||
|
DetectionClassesDict = detectionClassesDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key("n")] public string Name { get; set; } = null!;
|
||||||
|
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
|
||||||
|
[IgnoreMember]public TimeSpan Time { get; set; }
|
||||||
|
[IgnoreMember]public string ImageExtension { get; set; } = null!;
|
||||||
|
[IgnoreMember]public DateTime CreatedDate { get; set; }
|
||||||
|
[IgnoreMember]public string CreatedEmail { get; set; } = null!;
|
||||||
|
[IgnoreMember]public RoleEnum CreatedRole { get; set; }
|
||||||
|
[IgnoreMember]public SourceEnum Source { get; set; }
|
||||||
|
[IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; }
|
||||||
|
|
||||||
|
[IgnoreMember]public DateTime ValidateDate { get; set; }
|
||||||
|
[IgnoreMember]public string ValidateEmail { get; set; } = null!;
|
||||||
|
|
||||||
|
[Key("d")] public IEnumerable<Detection> Detections { get; set; } = null!;
|
||||||
|
[Key("t")] public long Milliseconds { get; set; }
|
||||||
|
|
||||||
|
[Key("lat")]public double Lat { get; set; }
|
||||||
|
[Key("lon")]public double Lon { get; set; }
|
||||||
|
|
||||||
|
#region Calculated
|
||||||
|
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
|
||||||
|
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
|
||||||
|
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||||
|
[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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AnnotationImage : Annotation
|
||||||
|
{
|
||||||
|
[Key("i")] public byte[] Image { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AnnotationStatus
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Created = 10,
|
||||||
|
Edited = 20,
|
||||||
|
Validated = 30,
|
||||||
|
Deleted = 40
|
||||||
|
}
|
||||||
@@ -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,6 +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<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,23 +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.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<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:";
|
||||||
@@ -30,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);
|
||||||
@@ -39,8 +46,8 @@ public class DbFactory : IDbFactory
|
|||||||
_memoryDataOptions = new DataOptions()
|
_memoryDataOptions = new DataOptions()
|
||||||
.UseDataProvider(SQLiteTools.GetDataProvider())
|
.UseDataProvider(SQLiteTools.GetDataProvider())
|
||||||
.UseConnection(_memoryConnection)
|
.UseConnection(_memoryConnection)
|
||||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
|
||||||
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
||||||
|
|
||||||
|
|
||||||
_fileConnection = new SQLiteConnection(FileConnStr);
|
_fileConnection = new SQLiteConnection(FileConnStr);
|
||||||
@@ -48,21 +55,33 @@ public class DbFactory : IDbFactory
|
|||||||
.UseDataProvider(SQLiteTools.GetDataProvider())
|
.UseDataProvider(SQLiteTools.GetDataProvider())
|
||||||
.UseConnection(_fileConnection)
|
.UseConnection(_fileConnection)
|
||||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||||
_ = _fileDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
|
|
||||||
|
|
||||||
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);
|
||||||
|
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
|
||||||
|
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
|
||||||
|
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
|
||||||
db.CreateTable<Annotation>();
|
db.CreateTable<Annotation>();
|
||||||
db.CreateTable<AnnotationName>();
|
if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
|
||||||
db.CreateTable<Detection>();
|
db.CreateTable<Detection>();
|
||||||
|
if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
|
||||||
|
db.CreateTable<AnnotationQueueRecord>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
@@ -71,38 +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 WriteSemaphore.WaitAsync();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await using var db = new AnnotationsDb(_memoryDataOptions);
|
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||||
await func(db);
|
await func(db);
|
||||||
}
|
ThrottleExt.Throttle(async () =>
|
||||||
|
|
||||||
public void SaveToDisk()
|
|
||||||
{
|
{
|
||||||
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
}
|
}
|
||||||
}
|
catch (Exception e)
|
||||||
|
|
||||||
public static class AnnotationsDbSchemaHolder
|
|
||||||
{
|
|
||||||
public static readonly MappingSchema MappingSchema;
|
|
||||||
|
|
||||||
static AnnotationsDbSchemaHolder()
|
|
||||||
{
|
{
|
||||||
MappingSchema = new MappingSchema();
|
_logger.LogError(e, e.Message);
|
||||||
var builder = new FluentMappingBuilder(MappingSchema);
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
WriteSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builder.Entity<Annotation>()
|
public async Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
{
|
||||||
.HasPrimaryKey(x => x.Name)
|
await WriteSemaphore.WaitAsync();
|
||||||
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
|
try
|
||||||
|
{
|
||||||
|
await using var db = new AnnotationsDb(_memoryDataOptions);
|
||||||
|
var result = await func(db);
|
||||||
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
WriteSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builder.Entity<Detection>()
|
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
|
||||||
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
{
|
||||||
|
await RunWrite(async db =>
|
||||||
builder.Entity<AnnotationName>()
|
{
|
||||||
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME);
|
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);
|
||||||
builder.Build();
|
_logger.LogInformation($"Deleted {detDeleted} detections, {annDeleted} annotations");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
-2
@@ -1,6 +1,7 @@
|
|||||||
using MediatR;
|
using Azaion.Common.Database;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
public class AnnotationCreatedEvent(Annotation annotation) : INotification
|
public class AnnotationCreatedEvent(Annotation annotation) : INotification
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
|
public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
|
||||||
|
{
|
||||||
|
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,13 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class AnnotatorControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
|
||||||
|
{
|
||||||
|
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DatasetExplorerControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
|
||||||
|
{
|
||||||
|
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
|
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
@@ -7,8 +8,14 @@ public static class BitmapExtensions
|
|||||||
{
|
{
|
||||||
public static async Task<BitmapImage> OpenImage(this string imagePath)
|
public static async Task<BitmapImage> OpenImage(this string imagePath)
|
||||||
{
|
{
|
||||||
var image = new BitmapImage();
|
|
||||||
await using var stream = File.OpenRead(imagePath);
|
await using var stream = File.OpenRead(imagePath);
|
||||||
|
return OpenImage(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BitmapImage OpenImage(this Stream stream)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var image = new BitmapImage();
|
||||||
image.BeginInit();
|
image.BeginInit();
|
||||||
image.CacheOption = BitmapCacheOption.OnLoad;
|
image.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
image.StreamSource = stream;
|
image.StreamSource = stream;
|
||||||
@@ -16,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,25 +4,15 @@ namespace Azaion.Common.Extensions;
|
|||||||
|
|
||||||
public static class ColorExtensions
|
public static class ColorExtensions
|
||||||
{
|
{
|
||||||
public static Color ToColor(this int id)
|
private const int MIN_ALPHA = 15;
|
||||||
|
private const int MAX_ALPHA = 150;
|
||||||
|
|
||||||
|
public static Color ToConfidenceColor(this Color color, double confidence = 1)
|
||||||
{
|
{
|
||||||
var index = id % ColorValues.Length;
|
color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA)));
|
||||||
var hex = index == -1
|
|
||||||
? "#40DDDDDD"
|
|
||||||
: $"#40{ColorValues[index]}";
|
|
||||||
var color =(Color)ColorConverter.ConvertFromString(hex);
|
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly string[] ColorValues =
|
public static Color ToColor(this string hexColor) =>
|
||||||
[
|
(Color)ColorConverter.ConvertFromString(hexColor);
|
||||||
"FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000",
|
|
||||||
"800000", "008000", "000080", "808000", "800080", "008080", "808080",
|
|
||||||
"C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0",
|
|
||||||
"400000", "004000", "000040", "404000", "400040", "004040", "404040",
|
|
||||||
"200000", "002000", "000020", "202000", "200020", "002020", "202020",
|
|
||||||
"600000", "006000", "000060", "606000", "600060", "006060", "606060",
|
|
||||||
"A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0",
|
|
||||||
"E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0"
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class GeoUtils
|
||||||
|
{
|
||||||
|
private const double EARTH_RADIUS = 6378137;
|
||||||
|
|
||||||
|
public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom)
|
||||||
|
{
|
||||||
|
var latRad = lat * Math.PI / 180.0;
|
||||||
|
var n = Math.Pow(2.0, zoom);
|
||||||
|
var xTile = (int)Math.Floor((lon + 180.0) / 360.0 * n);
|
||||||
|
var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
|
||||||
|
return (xTile, yTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 lonDeg = x / n * 360.0 - 180.0;
|
||||||
|
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
|
||||||
|
var latDeg = latRad * 180.0 / Math.PI;
|
||||||
|
return new GeoPoint(latDeg, lonDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
|
||||||
|
{
|
||||||
|
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
||||||
|
var minLat = Math.Max(centerGeoPoint.Lat - 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 minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
|
||||||
|
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
|
||||||
|
|
||||||
|
return (minLat, maxLat, minLon, maxLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class GraphicsExtensions
|
||||||
|
{
|
||||||
|
public static void DrawTextBox(this Graphics g, string text, PointF position, Brush background, Brush foreground)
|
||||||
|
{
|
||||||
|
using var textFont = new Font(FontFamily.GenericSerif, 14);
|
||||||
|
using var stringFormat = new StringFormat();
|
||||||
|
stringFormat.LineAlignment = StringAlignment.Near;
|
||||||
|
stringFormat.Alignment = StringAlignment.Center;
|
||||||
|
|
||||||
|
var padding = 1.0f;
|
||||||
|
var textSize = g.MeasureString(text, textFont);
|
||||||
|
|
||||||
|
var backgroundRect = new RectangleF(
|
||||||
|
position.X - textSize.Width / 2.0f - padding,
|
||||||
|
position.Y - padding,
|
||||||
|
textSize.Width + 2 * padding,
|
||||||
|
textSize.Height + 2 * padding
|
||||||
|
);
|
||||||
|
|
||||||
|
g.FillRectangle(background, backgroundRect);
|
||||||
|
g.DrawString(text, textFont, foreground, position, stringFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ public class ParallelExt
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var threadsCount = (int)(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
|
var threadsCount = (int)Math.Round(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
|
||||||
|
|
||||||
var processedCount = 0;
|
var processedCount = 0;
|
||||||
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
|
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string ToFName(this string path) =>
|
||||||
|
Path.GetFileNameWithoutExtension(path).Replace(" ", "");
|
||||||
|
|
||||||
|
public static string ToTimeName(this string fName, TimeSpan? ts) =>
|
||||||
|
$"{fName}_{ts:hmmssf}";
|
||||||
|
}
|
||||||
@@ -1,19 +1,71 @@
|
|||||||
namespace Azaion.Common.Extensions;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Extensions;
|
||||||
|
|
||||||
public static class ThrottleExt
|
public static class ThrottleExt
|
||||||
{
|
{
|
||||||
private static bool _throttleOn;
|
private class ThrottleState(Func<Task> action)
|
||||||
public static async Task Throttle(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
if (_throttleOn)
|
public Func<Task> Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
return;
|
public bool IsCoolingDown = false;
|
||||||
|
public bool CallScheduledDuringCooldown = false;
|
||||||
|
public Task CooldownTask = Task.CompletedTask;
|
||||||
|
public readonly object StateLock = new();
|
||||||
|
}
|
||||||
|
|
||||||
_throttleOn = true;
|
private static readonly ConcurrentDictionary<Guid, ThrottleState> ThrottlerStates = new();
|
||||||
await func();
|
|
||||||
_ = Task.Run(async () =>
|
public static void Throttle(Func<Task> action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false)
|
||||||
{
|
{
|
||||||
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
_throttleOn = false;
|
if (actionId == Guid.Empty)
|
||||||
}, cancellationToken);
|
throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId));
|
||||||
|
if (interval <= TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive.");
|
||||||
|
|
||||||
|
var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action));
|
||||||
|
state.Action = action;
|
||||||
|
|
||||||
|
lock (state.StateLock)
|
||||||
|
{
|
||||||
|
if (!state.IsCoolingDown)
|
||||||
|
{
|
||||||
|
state.IsCoolingDown = true;
|
||||||
|
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (scheduleCallAfterCooldown)
|
||||||
|
state.CallScheduledDuringCooldown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await state.Action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Task.Delay(interval);
|
||||||
|
lock (state.StateLock)
|
||||||
|
{
|
||||||
|
if (state.CallScheduledDuringCooldown)
|
||||||
|
{
|
||||||
|
state.CallScheduledDuringCooldown = false;
|
||||||
|
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.IsCoolingDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,15 +1,18 @@
|
|||||||
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;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Events;
|
||||||
using Azaion.CommonSecurity.Services;
|
using Azaion.Common.Extensions;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
|
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;
|
||||||
@@ -17,127 +20,251 @@ using RabbitMQ.Stream.Client.Reliable;
|
|||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public class AnnotationService
|
// 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 AzaionApiClient _apiClient;
|
|
||||||
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 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 SemaphoreSlim _imageAccessSemaphore = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
|
||||||
|
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
public AnnotationService(AzaionApiClient apiClient,
|
|
||||||
|
public AnnotationService(
|
||||||
IDbFactory dbFactory,
|
IDbFactory dbFactory,
|
||||||
FailsafeAnnotationsProducer producer,
|
FailsafeAnnotationsProducer producer,
|
||||||
IOptions<QueueConfig> queueConfig,
|
IOptions<QueueConfig> queueConfig,
|
||||||
|
IOptions<UIConfig> uiConfig,
|
||||||
IGalleryService galleryService,
|
IGalleryService galleryService,
|
||||||
IMediator mediator)
|
IMediator mediator,
|
||||||
|
IAzaionApi api,
|
||||||
|
ILogger<AnnotationService> logger)
|
||||||
{
|
{
|
||||||
_apiClient = apiClient;
|
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_producer = producer;
|
_producer = producer;
|
||||||
_galleryService = galleryService;
|
_galleryService = galleryService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_api = api;
|
||||||
|
_logger = logger;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
|
_uiConfig = uiConfig.Value;
|
||||||
|
|
||||||
Task.Run(async () => await Init()).Wait();
|
Task.Run(async () => await InitQueueConsumer()).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init()
|
private async Task InitQueueConsumer(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
if (!_api.CurrentUser.Role.IsValidator())
|
||||||
|
return;
|
||||||
|
|
||||||
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
|
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
|
||||||
{
|
{
|
||||||
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
|
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
|
||||||
UserName = _queueConfig.ConsumerUsername,
|
UserName = _queueConfig.ConsumerUsername,
|
||||||
Password = _queueConfig.ConsumerPassword
|
Password = _queueConfig.ConsumerPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets();
|
||||||
|
|
||||||
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
||||||
{
|
{
|
||||||
OffsetSpec = new OffsetTypeFirst(),
|
Reference = _api.CurrentUser.Email,
|
||||||
MessageHandler = async (stream, _, _, message) =>
|
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
|
||||||
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)),
|
MessageHandler = async (_, _, context, message) =>
|
||||||
|
{
|
||||||
|
await _messageProcessingSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
|
||||||
|
if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
|
||||||
|
{
|
||||||
|
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
|
||||||
|
{
|
||||||
|
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
|
||||||
|
await SaveAnnotationInner(
|
||||||
|
msg.CreatedDate,
|
||||||
|
msg.OriginalMediaName,
|
||||||
|
msg.Name,
|
||||||
|
msg.Time,
|
||||||
|
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
|
||||||
|
msg.Source,
|
||||||
|
msg.Image == null ? null : new MemoryStream(msg.Image),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//AI / Manual
|
//AI
|
||||||
public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) =>
|
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
|
||||||
await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
|
|
||||||
|
|
||||||
//Queue (only from operators)
|
|
||||||
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator)
|
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
|
||||||
return;
|
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);
|
||||||
await SaveAnnotationInner(
|
|
||||||
message.CreatedDate,
|
|
||||||
message.Name,
|
|
||||||
message.ImageExtension,
|
|
||||||
JsonConvert.DeserializeObject<List<Detection>>(message.Detections) ?? [],
|
|
||||||
message.Source,
|
|
||||||
new MemoryStream(message.Image),
|
|
||||||
message.CreatedRole,
|
|
||||||
message.CreatedEmail,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream,
|
//Manual
|
||||||
RoleEnum createdRole,
|
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, annotationName, time, detections, SourceEnum.Manual, stream,
|
||||||
|
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
|
||||||
|
|
||||||
|
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, string annotationName, TimeSpan time,
|
||||||
|
List<Detection> detections, SourceEnum source, Stream? stream,
|
||||||
|
RoleEnum userRole,
|
||||||
string createdEmail,
|
string createdEmail,
|
||||||
|
ulong? offset = null,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
//Flow for roles:
|
var status = AnnotationStatus.Created;
|
||||||
// Operator:
|
var annotation = await _dbFactory.RunWrite(async db =>
|
||||||
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
|
|
||||||
// Validator:
|
|
||||||
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
|
|
||||||
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
|
|
||||||
|
|
||||||
var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
|
||||||
AnnotationStatus status;
|
|
||||||
|
|
||||||
var annotation = await _dbFactory.Run(async db =>
|
|
||||||
{
|
{
|
||||||
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
var ann = await db.Annotations
|
||||||
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator
|
.LoadWith(x => x.Detections)
|
||||||
? AnnotationStatus.Validated
|
.FirstOrDefaultAsync(x => x.Name == annotationName, token: token);
|
||||||
: AnnotationStatus.Created;
|
|
||||||
|
|
||||||
if (ann != null)
|
await db.Detections.DeleteAsync(x => x.AnnotationName == annotationName, token: token);
|
||||||
await db.Annotations
|
|
||||||
.Where(x => x.Name == fName)
|
if (ann != null) //Annotation is already exists
|
||||||
.Set(x => x.Classes, classes)
|
{
|
||||||
.Set(x => x.Source, source)
|
status = AnnotationStatus.Edited;
|
||||||
|
|
||||||
|
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)
|
||||||
.UpdateAsync(token: token);
|
.UpdateAsync(token: token);
|
||||||
|
|
||||||
|
ann.Detections = detections;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ann = new Annotation
|
ann = new Annotation
|
||||||
{
|
{
|
||||||
CreatedDate = createdDate,
|
CreatedDate = createdDate,
|
||||||
Name = fName,
|
Name = annotationName,
|
||||||
ImageExtension = imageExtension,
|
OriginalMediaName = originalMediaName,
|
||||||
|
Time = time,
|
||||||
|
ImageExtension = Constants.JPG_EXT,
|
||||||
CreatedEmail = createdEmail,
|
CreatedEmail = createdEmail,
|
||||||
CreatedRole = createdRole,
|
CreatedRole = userRole,
|
||||||
AnnotationStatus = status,
|
AnnotationStatus = status,
|
||||||
Source = source,
|
Source = source,
|
||||||
Detections = detections
|
Detections = detections
|
||||||
};
|
};
|
||||||
await db.InsertAsync(ann, token: token);
|
await db.InsertAsync(ann, token: token);
|
||||||
}
|
}
|
||||||
|
await db.BulkCopyAsync(detections, cancellationToken: token);
|
||||||
return ann;
|
return ann;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Save image should be done in 1 thread only
|
||||||
|
await _imageAccessSemaphore.WaitAsync(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Image image = null!;
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
var img = System.Drawing.Image.FromStream(stream);
|
image = Image.FromStream(stream);
|
||||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
if (File.Exists(annotation.ImagePath))
|
||||||
|
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
|
||||||
|
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
|
||||||
}
|
}
|
||||||
|
|
||||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
||||||
await _galleryService.CreateThumbnail(annotation, token);
|
|
||||||
await _producer.SendToQueue(annotation, 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 _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
||||||
|
|
||||||
|
if (!offset.HasValue) //Send to queue only if we're not getting from queue already
|
||||||
|
await _producer.SendToInnerQueue([annotation.Name], status, token);
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!_api.CurrentUser.Role.IsValidator())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var annNames = annotationNames.ToHashSet();
|
||||||
|
await _dbFactory.RunWrite(async db =>
|
||||||
|
{
|
||||||
|
await db.Annotations
|
||||||
|
.Where(x => annNames.Contains(x.Name))
|
||||||
|
.Set(x => x.AnnotationStatus, AnnotationStatus.Validated)
|
||||||
|
.Set(x => x.ValidateDate, DateTime.UtcNow)
|
||||||
|
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
|
||||||
|
.UpdateAsync(token: token);
|
||||||
|
});
|
||||||
|
if (!fromQueue)
|
||||||
|
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IAnnotationService
|
||||||
|
{
|
||||||
|
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
|
||||||
|
Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
|
||||||
|
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IAzaionApi
|
||||||
|
{
|
||||||
|
ApiCredentials Credentials { get; }
|
||||||
|
User CurrentUser { get; }
|
||||||
|
void UpdateOffsets(UserQueueOffsets offsets);
|
||||||
|
//Stream GetResource(string filename, string folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AzaionApi(ILogger logger, HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
|
||||||
|
{
|
||||||
|
private string _jwtToken = null!;
|
||||||
|
const string APP_JSON = "application/json";
|
||||||
|
public ApiCredentials Credentials => credentials;
|
||||||
|
|
||||||
|
public User CurrentUser
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var user = cache.GetFromCache(Constants.CURRENT_USER_CACHE_KEY,
|
||||||
|
() => Get<User>("users/current"));
|
||||||
|
if (user == null)
|
||||||
|
throw new Exception("Can't get current user");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateOffsets(UserQueueOffsets offsets)
|
||||||
|
{
|
||||||
|
Put($"/users/queue-offsets/set", new
|
||||||
|
{
|
||||||
|
Email = CurrentUser.Email,
|
||||||
|
Offsets = offsets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage Send(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_jwtToken))
|
||||||
|
Authorize();
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
|
||||||
|
var response = client.Send(request);
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Authorize();
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
|
||||||
|
response = client.Send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return response;
|
||||||
|
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var content = new StreamReader(stream).ReadToEnd();
|
||||||
|
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
|
||||||
|
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
|
||||||
|
}
|
||||||
|
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? Get<T>(string url)
|
||||||
|
{
|
||||||
|
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var json = new StreamReader(stream).ReadToEnd();
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Put<T>(string url, T obj)
|
||||||
|
{
|
||||||
|
Send(new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
|
{
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Authorize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
|
||||||
|
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email = credentials.Email,
|
||||||
|
password = credentials.Password
|
||||||
|
};
|
||||||
|
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
|
||||||
|
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
|
||||||
|
var response = client.Send(message);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
|
||||||
|
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var json = new StreamReader(stream).ReadToEnd();
|
||||||
|
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(result?.Token))
|
||||||
|
throw new Exception("JWT Token not found in response");
|
||||||
|
|
||||||
|
_jwtToken = result.Token;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.Error(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using LazyCache;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface ICache
|
||||||
|
{
|
||||||
|
T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null);
|
||||||
|
void Invalidate(string key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MemoryCache : ICache
|
||||||
|
{
|
||||||
|
private readonly IAppCache _cache = new CachingService();
|
||||||
|
|
||||||
|
public T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null)
|
||||||
|
{
|
||||||
|
expiration ??= TimeSpan.FromHours(4);
|
||||||
|
return _cache.GetOrAdd(key, entry =>
|
||||||
|
{
|
||||||
|
var result = fetchFunc();
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = expiration;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate(string key) => _cache.Remove(key);
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ using Azaion.Common.Database;
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.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;
|
||||||
@@ -18,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,83 +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 GetFromQueue(cancellationToken);
|
|
||||||
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
|
||||||
{
|
{
|
||||||
var sent = false;
|
var sent = false;
|
||||||
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send
|
while (!sent || !ct.IsCancellationRequested) //Waiting for send
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var createdMessages = messagesChunk
|
var (records, annotationsDict) = await _dbFactory.Run(async db =>
|
||||||
.Where(x => x.Status == AnnotationStatus.Created)
|
{
|
||||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
|
||||||
|
var editedCreatedNames = records
|
||||||
|
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
|
||||||
|
.Select(x => x.AnnotationNames.FirstOrDefault())
|
||||||
.ToList();
|
.ToList();
|
||||||
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
|
||||||
|
|
||||||
var validatedMessages = messagesChunk
|
var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
|
||||||
.Where(x => x.Status == AnnotationStatus.Validated)
|
.Where(x => editedCreatedNames.Contains(x.Name))
|
||||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
.ToDictionaryAsync(a => a.Name, token: ct);
|
||||||
.ToList();
|
return (records, annotationsDict);
|
||||||
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
|
});
|
||||||
|
|
||||||
await _dbFactory.Run(async db =>
|
var messages = new List<Message>();
|
||||||
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
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;
|
sent = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, e.Message);
|
_logger.LogError(e, e.Message);
|
||||||
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
|
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||||
}
|
}
|
||||||
}
|
await Task.Delay(TimeSpan.FromSeconds(10), ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default)
|
public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _dbFactory.Run(async db =>
|
if (_uiConfig.SilentDetection)
|
||||||
|
return;
|
||||||
|
await _dbFactory.RunWrite(async db =>
|
||||||
|
await db.InsertAsync(new AnnotationQueueRecord
|
||||||
{
|
{
|
||||||
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a)
|
Id = Guid.NewGuid(),
|
||||||
.ToListAsync(token: cancellationToken);
|
DateTime = DateTime.UtcNow,
|
||||||
|
Operation = status,
|
||||||
var messages = new List<AnnotationCreatedMessage>();
|
AnnotationNames = annotationNames
|
||||||
foreach (var annotation in annotations)
|
}, token: cancellationToken));
|
||||||
{
|
|
||||||
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
|
||||||
var annCreateMessage = new AnnotationCreatedMessage
|
|
||||||
{
|
|
||||||
Name = annotation.Name,
|
|
||||||
|
|
||||||
CreatedRole = annotation.CreatedRole,
|
|
||||||
CreatedEmail = annotation.CreatedEmail,
|
|
||||||
CreatedDate = annotation.CreatedDate,
|
|
||||||
|
|
||||||
Image = image,
|
|
||||||
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
|
||||||
Source = annotation.Source
|
|
||||||
};
|
|
||||||
messages.Add(annCreateMessage);
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await _dbFactory.Run(async db =>
|
|
||||||
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ using Azaion.Common.Database;
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Extensions;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -60,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);
|
||||||
@@ -72,10 +72,9 @@ public class GalleryService(
|
|||||||
await _updateLock.WaitAsync();
|
await _updateLock.WaitAsync();
|
||||||
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
||||||
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
||||||
var missedAnnotations = new ConcurrentBag<Annotation>();
|
var missedAnnotations = new ConcurrentDictionary<string, Annotation>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
|
var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
|
||||||
|
|
||||||
var thumbnails = ThumbnailsDirectory.GetFiles()
|
var thumbnails = ThumbnailsDirectory.GetFiles()
|
||||||
@@ -89,7 +88,7 @@ public class GalleryService(
|
|||||||
|
|
||||||
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
|
||||||
{
|
{
|
||||||
var fName = Path.GetFileNameWithoutExtension(file.Name);
|
var fName = file.Name.ToFName();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
|
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
|
||||||
@@ -97,6 +96,7 @@ public class GalleryService(
|
|||||||
{
|
{
|
||||||
File.Delete(file.FullName);
|
File.Delete(file.FullName);
|
||||||
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
|
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
|
||||||
|
await dbFactory.DeleteAnnotations([fName], cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +105,36 @@ public class GalleryService(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
|
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
|
||||||
|
|
||||||
|
//get names and time
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(file.Name);
|
||||||
|
var strings = fileName.Split("_");
|
||||||
|
var timeStr = strings.LastOrDefault();
|
||||||
|
|
||||||
|
string originalMediaName;
|
||||||
|
TimeSpan time;
|
||||||
|
|
||||||
|
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
|
||||||
|
if (!string.IsNullOrEmpty(timeStr) &&
|
||||||
|
timeStr.Length == 6 &&
|
||||||
|
int.TryParse(timeStr[..1], out var hours) &&
|
||||||
|
int.TryParse(timeStr[1..3], out var minutes) &&
|
||||||
|
int.TryParse(timeStr[3..5], out var seconds) &&
|
||||||
|
int.TryParse(timeStr[5..], out var milliseconds))
|
||||||
|
{
|
||||||
|
time = new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
|
||||||
|
originalMediaName = fileName[..^7];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
originalMediaName = fileName;
|
||||||
|
time = TimeSpan.FromSeconds(0);
|
||||||
|
}
|
||||||
|
|
||||||
var annotation = new Annotation
|
var annotation = new Annotation
|
||||||
{
|
{
|
||||||
|
Time = time,
|
||||||
|
OriginalMediaName = originalMediaName,
|
||||||
Name = fName,
|
Name = fName,
|
||||||
ImageExtension = Path.GetExtension(file.Name),
|
ImageExtension = Path.GetExtension(file.Name),
|
||||||
Detections = detections,
|
Detections = detections,
|
||||||
@@ -117,11 +145,18 @@ public class GalleryService(
|
|||||||
AnnotationStatus = AnnotationStatus.Validated
|
AnnotationStatus = AnnotationStatus.Validated
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Remove duplicates
|
||||||
if (!existingAnnotations.ContainsKey(fName))
|
if (!existingAnnotations.ContainsKey(fName))
|
||||||
missedAnnotations.Add(annotation);
|
{
|
||||||
|
if (missedAnnotations.ContainsKey(fName))
|
||||||
|
logger.LogInformation($"{fName} is already exists! Duplicate!");
|
||||||
|
else
|
||||||
|
missedAnnotations.TryAdd(fName, annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!thumbnails.Contains(fName))
|
if (!thumbnails.Contains(fName))
|
||||||
await CreateThumbnail(annotation, cancellationToken);
|
await CreateThumbnail(annotation, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -133,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;
|
||||||
@@ -142,35 +177,43 @@ public class GalleryService(
|
|||||||
ProgressUpdateInterval = 200
|
ProgressUpdateInterval = 200
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, $"Failed to refresh thumbnails! Error: {e.Message}");
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
var copyOptions = new BulkCopyOptions
|
var copyOptions = new BulkCopyOptions
|
||||||
{
|
{
|
||||||
MaxBatchSize = 50
|
MaxBatchSize = 50
|
||||||
};
|
};
|
||||||
await dbFactory.Run(async db =>
|
|
||||||
{
|
//Db could be updated during the long files scraping
|
||||||
var xx = missedAnnotations.GroupBy(x => x.Name)
|
existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
||||||
.Where(gr => gr.Count() > 1)
|
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
||||||
|
var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList();
|
||||||
|
var annotationsToInsert = missedAnnotations
|
||||||
|
.Where(a => !existingAnnotations.ContainsKey(a.Key))
|
||||||
|
.Select(x => x.Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
foreach (var gr in xx)
|
|
||||||
Console.WriteLine(gr.Key);
|
await dbFactory.RunWrite(async db =>
|
||||||
await db.BulkCopyAsync(copyOptions, missedAnnotations);
|
{
|
||||||
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections));
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -194,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;
|
||||||
@@ -227,8 +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));
|
||||||
|
|
||||||
var rectangle = new RectangleF((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));
|
||||||
g.FillRectangle(brush, rectangle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
|
||||||
@@ -238,14 +280,36 @@ 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)
|
||||||
|
{
|
||||||
|
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
|
||||||
|
|
||||||
|
using var g = Graphics.FromImage(originalImage);
|
||||||
|
foreach (var detection in annotation.Detections)
|
||||||
|
{
|
||||||
|
var detClass = _annotationConfig.DetectionClassesDict[detection.ClassNumber];
|
||||||
|
var color = detClass.Color;
|
||||||
|
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));
|
||||||
|
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}%";
|
||||||
|
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user