mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 22:06:30 +00:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fce10eb39 | |||
| 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 | |||
| 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 | |||
| 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 |
@@ -6,3 +6,11 @@ obj
|
|||||||
*.user
|
*.user
|
||||||
log*.txt
|
log*.txt
|
||||||
secured-config
|
secured-config
|
||||||
|
build
|
||||||
|
venv
|
||||||
|
*.c
|
||||||
|
*.pyd
|
||||||
|
cython_debug*
|
||||||
|
dist
|
||||||
|
AzaionSuiteInstaller.exe
|
||||||
|
azaion\.*\.big
|
||||||
+120
-27
@@ -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
|
||||||
@@ -148,14 +154,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>
|
||||||
@@ -221,7 +235,6 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.RowSpan="4"
|
Grid.RowSpan="4"
|
||||||
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 +243,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 +267,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 +298,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 +313,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="0" /> <!-- 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">
|
||||||
@@ -480,7 +499,7 @@
|
|||||||
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="AutoDetect">
|
||||||
<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
|
||||||
@@ -508,8 +527,82 @@
|
|||||||
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>
|
||||||
|
<StatusBar Grid.Column="14"
|
||||||
Background="#252525"
|
Background="#252525"
|
||||||
Foreground="White">
|
Foreground="White">
|
||||||
<StatusBar.ItemsPanel>
|
<StatusBar.ItemsPanel>
|
||||||
|
|||||||
+303
-353
@@ -6,21 +6,20 @@ using System.Windows.Controls;
|
|||||||
using System.Windows.Controls.Primitives;
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using Azaion.Annotator.DTO;
|
using Azaion.Annotator.DTO;
|
||||||
using Azaion.Annotator.Extensions;
|
|
||||||
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.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Size = System.Windows.Size;
|
using Size = System.Windows.Size;
|
||||||
using IntervalTree;
|
using IntervalTree;
|
||||||
|
using LinqToDB;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
|
||||||
@@ -38,37 +37,45 @@ public partial class Annotator
|
|||||||
private readonly IConfigUpdater _configUpdater;
|
private readonly IConfigUpdater _configUpdater;
|
||||||
private readonly HelpWindow _helpWindow;
|
private readonly HelpWindow _helpWindow;
|
||||||
private readonly ILogger<Annotator> _logger;
|
private readonly ILogger<Annotator> _logger;
|
||||||
private readonly VLCFrameExtractor _vlcFrameExtractor;
|
|
||||||
private readonly IAIDetector _aiDetector;
|
|
||||||
private readonly AnnotationService _annotationService;
|
private readonly AnnotationService _annotationService;
|
||||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
private readonly IDbFactory _dbFactory;
|
||||||
|
private readonly IInferenceService _inferenceService;
|
||||||
|
|
||||||
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
|
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
|
||||||
private bool _suspendLayout;
|
private bool _suspendLayout;
|
||||||
|
private bool _gpsPanelVisible = false;
|
||||||
|
|
||||||
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100);
|
public readonly CancellationTokenSource MainCancellationSource = new();
|
||||||
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300);
|
public CancellationTokenSource DetectionCancellationSource = new();
|
||||||
|
public bool FollowAI = false;
|
||||||
|
public bool IsInferenceNow = false;
|
||||||
|
|
||||||
|
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
|
||||||
|
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
|
||||||
|
private readonly IGpsMatcherService _gpsMatcherService;
|
||||||
|
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
|
||||||
|
|
||||||
private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
|
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
|
||||||
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
|
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
|
||||||
|
|
||||||
public IntervalTree<TimeSpan, List<Detection>> Detections { get; set; } = new();
|
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
|
||||||
private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
|
|
||||||
|
|
||||||
public Annotator(
|
public Annotator(
|
||||||
IConfigUpdater configUpdater,
|
IConfigUpdater configUpdater,
|
||||||
IOptions<AppConfig> appConfig,
|
IOptions<AppConfig> appConfig,
|
||||||
LibVLC libVLC, MediaPlayer mediaPlayer,
|
LibVLC libVLC,
|
||||||
|
MediaPlayer mediaPlayer,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
FormState formState,
|
FormState formState,
|
||||||
HelpWindow helpWindow,
|
HelpWindow helpWindow,
|
||||||
ILogger<Annotator> logger,
|
ILogger<Annotator> logger,
|
||||||
VLCFrameExtractor vlcFrameExtractor,
|
AnnotationService annotationService,
|
||||||
IAIDetector aiDetector,
|
IDbFactory dbFactory,
|
||||||
AnnotationService annotationService)
|
IInferenceService inferenceService,
|
||||||
|
IGpsMatcherService gpsMatcherService)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
_appConfig = appConfig.Value;
|
_appConfig = appConfig.Value;
|
||||||
_configUpdater = configUpdater;
|
_configUpdater = configUpdater;
|
||||||
_libVLC = libVLC;
|
_libVLC = libVLC;
|
||||||
@@ -77,14 +84,32 @@ public partial class Annotator
|
|||||||
_formState = formState;
|
_formState = formState;
|
||||||
_helpWindow = helpWindow;
|
_helpWindow = helpWindow;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_vlcFrameExtractor = vlcFrameExtractor;
|
|
||||||
_aiDetector = aiDetector;
|
|
||||||
_annotationService = annotationService;
|
_annotationService = annotationService;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_inferenceService = inferenceService;
|
||||||
|
_gpsMatcherService = gpsMatcherService;
|
||||||
|
|
||||||
Loaded += OnLoaded;
|
Loaded += OnLoaded;
|
||||||
Closed += OnFormClosed;
|
Closed += OnFormClosed;
|
||||||
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
|
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
|
||||||
|
TbFolder.TextChanged += async (sender, args) =>
|
||||||
|
{
|
||||||
|
if (!Path.Exists(TbFolder.Text))
|
||||||
|
return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
|
||||||
|
await ReloadFiles();
|
||||||
|
await SaveUserSettings();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
|
||||||
|
MapMatcherComponent.Init(_appConfig, _gpsMatcherService);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
@@ -94,16 +119,13 @@ public partial class Annotator
|
|||||||
|
|
||||||
_suspendLayout = true;
|
_suspendLayout = true;
|
||||||
|
|
||||||
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.LeftPanelWidth);
|
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.UIConfig.LeftPanelWidth);
|
||||||
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth);
|
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.UIConfig.RightPanelWidth);
|
||||||
|
|
||||||
_suspendLayout = false;
|
_suspendLayout = false;
|
||||||
|
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
|
||||||
|
|
||||||
ReloadFiles();
|
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
|
||||||
|
|
||||||
AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
|
|
||||||
LvClasses.ItemsSource = AnnotationClasses;
|
|
||||||
LvClasses.SelectedIndex = 0;
|
|
||||||
|
|
||||||
if (LvFiles.Items.IsEmpty)
|
if (LvFiles.Items.IsEmpty)
|
||||||
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
|
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
|
||||||
@@ -141,7 +163,7 @@ public partial class Annotator
|
|||||||
_formState.CurrentVideoSize = new Size(vw, vh);
|
_formState.CurrentVideoSize = new Size(vw, vh);
|
||||||
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
|
||||||
|
|
||||||
await Dispatcher.Invoke(async () => await ReloadAnnotations(_cancellationTokenSource.Token));
|
await Dispatcher.Invoke(async () => await ReloadAnnotations());
|
||||||
|
|
||||||
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
|
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
|
||||||
{
|
{
|
||||||
@@ -151,11 +173,16 @@ public partial class Annotator
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
LvFiles.MouseDoubleClick += async (_, _) => await _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play));
|
LvFiles.MouseDoubleClick += async (_, _) =>
|
||||||
|
|
||||||
LvClasses.SelectionChanged += (_, _) =>
|
|
||||||
{
|
{
|
||||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
if (IsInferenceNow)
|
||||||
|
FollowAI = false;
|
||||||
|
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
|
||||||
|
};
|
||||||
|
|
||||||
|
LvClasses.DetectionClassChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
var selectedClass = args.DetectionClass;
|
||||||
Editor.CurrentAnnClass = selectedClass;
|
Editor.CurrentAnnClass = selectedClass;
|
||||||
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
|
||||||
};
|
};
|
||||||
@@ -179,10 +206,12 @@ public partial class Annotator
|
|||||||
DgAnnotations.MouseDoubleClick += (sender, args) =>
|
DgAnnotations.MouseDoubleClick += (sender, args) =>
|
||||||
{
|
{
|
||||||
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
|
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
|
||||||
OpenAnnotationResult((AnnotationResult)dgRow!.Item);
|
if (dgRow != null)
|
||||||
|
OpenAnnotationResult((AnnotationResult)dgRow!.Item);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
DgAnnotations.KeyUp += (sender, args) =>
|
DgAnnotations.KeyUp += async (sender, args) =>
|
||||||
{
|
{
|
||||||
switch (args.Key)
|
switch (args.Key)
|
||||||
{
|
{
|
||||||
@@ -196,17 +225,9 @@ public partial class Annotator
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
|
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
|
||||||
foreach (var annotationResult in res)
|
var annotations = res.Select(x => x.Annotation).ToList();
|
||||||
{
|
|
||||||
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
|
|
||||||
var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
|
|
||||||
File.Delete(annotationResult.Image);
|
|
||||||
|
|
||||||
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt"));
|
await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
|
||||||
File.Delete(thumbnailPath);
|
|
||||||
_formState.AnnotationResults.Remove(annotationResult);
|
|
||||||
Detections.Remove(Detections.Query(annotationResult.Time));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -217,32 +238,35 @@ public partial class Annotator
|
|||||||
|
|
||||||
public void OpenAnnotationResult(AnnotationResult res)
|
public void OpenAnnotationResult(AnnotationResult res)
|
||||||
{
|
{
|
||||||
|
if (IsInferenceNow)
|
||||||
|
FollowAI = false;
|
||||||
_mediaPlayer.SetPause(true);
|
_mediaPlayer.SetPause(true);
|
||||||
Editor.RemoveAllAnns();
|
Editor.RemoveAllAnns();
|
||||||
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds;
|
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
|
||||||
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||||
Editor.ClearExpiredAnnotations(res.Time);
|
Editor.ClearExpiredAnnotations(res.Annotation.Time);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAnnotationsToCanvas(res.Time, res.Detections, showImage: true);
|
ShowAnnotations(res.Annotation, showImage: true);
|
||||||
}
|
}
|
||||||
private async Task SaveUserSettings()
|
private Task SaveUserSettings()
|
||||||
{
|
{
|
||||||
if (_suspendLayout)
|
if (_suspendLayout)
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_appConfig.AnnotationConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
|
||||||
_appConfig.AnnotationConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
|
||||||
|
|
||||||
await ThrottleExt.Throttle(() =>
|
ThrottleExt.Throttle(() =>
|
||||||
{
|
{
|
||||||
_configUpdater.Save(_appConfig);
|
_configUpdater.Save(_appConfig);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}, TimeSpan.FromSeconds(5));
|
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowTimeAnnotations(TimeSpan time)
|
private void ShowTimeAnnotations(TimeSpan time)
|
||||||
@@ -254,134 +278,93 @@ public partial class Annotator
|
|||||||
Editor.ClearExpiredAnnotations(time);
|
Editor.ClearExpiredAnnotations(time);
|
||||||
});
|
});
|
||||||
|
|
||||||
var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x));
|
ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault());
|
||||||
AddAnnotationsToCanvas(time, annotations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAnnotationsToCanvas(TimeSpan? time, IEnumerable<Detection> labels, bool showImage = false)
|
private void ShowAnnotations(Annotation? annotation, bool showImage = false)
|
||||||
{
|
{
|
||||||
|
if (annotation == null)
|
||||||
|
return;
|
||||||
Dispatcher.Invoke(async () =>
|
Dispatcher.Invoke(async () =>
|
||||||
{
|
{
|
||||||
var canvasSize = Editor.RenderSize;
|
|
||||||
var videoSize = _formState.CurrentVideoSize;
|
var videoSize = _formState.CurrentVideoSize;
|
||||||
if (showImage)
|
if (showImage)
|
||||||
{
|
{
|
||||||
var fName = _formState.GetTimeName(time);
|
if (File.Exists(annotation.ImagePath))
|
||||||
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
|
|
||||||
if (File.Exists(imgPath))
|
|
||||||
{
|
{
|
||||||
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
|
||||||
_formState.BackgroundTime = time;
|
_formState.BackgroundTime = annotation.Time;
|
||||||
videoSize = Editor.RenderSize;
|
videoSize = Editor.RenderSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach (var label in labels)
|
Editor.CreateDetections(annotation.Time, annotation.Detections, _appConfig.AnnotationConfig.DetectionClasses, videoSize);
|
||||||
{
|
|
||||||
var annClass = _appConfig.AnnotationConfig.AnnotationClasses[label.ClassNumber];
|
|
||||||
var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability);
|
|
||||||
Editor.CreateAnnotation(annClass, time, canvasLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadAnnotations(CancellationToken ct = default)
|
private async Task ReloadAnnotations()
|
||||||
{
|
{
|
||||||
_formState.AnnotationResults.Clear();
|
_formState.AnnotationResults.Clear();
|
||||||
Detections.Clear();
|
TimedAnnotations.Clear();
|
||||||
Editor.RemoveAllAnns();
|
Editor.RemoveAllAnns();
|
||||||
|
|
||||||
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory);
|
var annotations = await _dbFactory.Run(async db =>
|
||||||
if (!labelDir.Exists)
|
await db.Annotations.LoadWith(x => x.Detections)
|
||||||
return;
|
.Where(x => x.OriginalMediaName == _formState.VideoName)
|
||||||
|
.OrderBy(x => x.Time)
|
||||||
|
.ToListAsync(token: MainCancellationSource.Token));
|
||||||
|
|
||||||
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt");
|
TimedAnnotations.Clear();
|
||||||
foreach (var file in labelFiles)
|
_formState.AnnotationResults.Clear();
|
||||||
await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct);
|
foreach (var ann in annotations)
|
||||||
|
{
|
||||||
|
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
|
||||||
|
_formState.AnnotationResults.Add(new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, ann));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Load from yolo label file
|
|
||||||
public async Task AddAnnotations(string name, List<YoloLabel> annotations, CancellationToken ct = default)
|
|
||||||
=> await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct);
|
|
||||||
|
|
||||||
//Add manually
|
//Add manually
|
||||||
public async Task AddAnnotations(string name, List<Detection> detections, CancellationToken ct = default)
|
public void AddAnnotation(Annotation annotation)
|
||||||
{
|
{
|
||||||
var time = Constants.GetTime(name);
|
var time = annotation.Time;
|
||||||
var timeValue = time ?? TimeSpan.FromMinutes(0);
|
var previousAnnotations = TimedAnnotations.Query(time);
|
||||||
var previousAnnotations = Detections.Query(timeValue);
|
TimedAnnotations.Remove(previousAnnotations);
|
||||||
Detections.Remove(previousAnnotations);
|
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
|
||||||
Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections);
|
|
||||||
|
|
||||||
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
|
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time);
|
||||||
if (existingResult != null)
|
if (existingResult != null)
|
||||||
_formState.AnnotationResults.Remove(existingResult);
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_formState.AnnotationResults.Remove(existingResult);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var dict = _formState.AnnotationResults
|
var dict = _formState.AnnotationResults
|
||||||
.Select((x, i) => new { x.Time, Index = i })
|
.Select((x, i) => new { x.Annotation.Time, Index = i })
|
||||||
.ToDictionary(x => x.Time, x => x.Index);
|
.ToDictionary(x => x.Time, x => x.Index);
|
||||||
|
|
||||||
var index = dict.Where(x => x.Key < timeValue)
|
var index = dict.Where(x => x.Key < time)
|
||||||
.OrderBy(x => timeValue - x.Key)
|
.OrderBy(x => time - x.Key)
|
||||||
.Select(x => x.Value + 1)
|
.Select(x => x.Value + 1)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
_formState.AnnotationResults.Insert(index, CreateAnnotationReult(timeValue, detections));
|
var annRes = new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation);
|
||||||
await File.WriteAllTextAsync($"{_appConfig.DirectoriesConfig.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct);
|
_formState.AnnotationResults.Insert(index, annRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AnnotationResult CreateAnnotationReult(TimeSpan timeValue, List<Detection> detections)
|
private async Task ReloadFiles()
|
||||||
{
|
|
||||||
var annotationResult = new AnnotationResult
|
|
||||||
{
|
|
||||||
Time = timeValue,
|
|
||||||
Image = $"{_formState.GetTimeName(timeValue)}.jpg",
|
|
||||||
Detections = detections,
|
|
||||||
};
|
|
||||||
if (detections.Count <= 0)
|
|
||||||
return annotationResult;
|
|
||||||
|
|
||||||
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
|
|
||||||
{
|
|
||||||
if (detections.Count == 0)
|
|
||||||
return (-1).ToColor();
|
|
||||||
|
|
||||||
return colorNumber >= detectionClasses.Count
|
|
||||||
? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color
|
|
||||||
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color;
|
|
||||||
}
|
|
||||||
|
|
||||||
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
|
|
||||||
|
|
||||||
annotationResult.ClassName = detectionClasses.Count > 1
|
|
||||||
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName))
|
|
||||||
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.FirstOrDefault()].Name;
|
|
||||||
|
|
||||||
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
|
|
||||||
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
|
|
||||||
annotationResult.ClassColor2 = GetAnnotationClass(detectionClasses, 2);
|
|
||||||
annotationResult.ClassColor3 = GetAnnotationClass(detectionClasses, 3);
|
|
||||||
return annotationResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReloadFiles()
|
|
||||||
{
|
{
|
||||||
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
|
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
|
||||||
if (!dir.Exists)
|
if (!dir.Exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var labelNames = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory).GetFiles()
|
|
||||||
.Select(x =>
|
|
||||||
{
|
|
||||||
var name = Path.GetFileNameWithoutExtension(x.Name);
|
|
||||||
return name.Length > 8
|
|
||||||
? name[..^7]
|
|
||||||
: name;
|
|
||||||
})
|
|
||||||
.GroupBy(x => x)
|
|
||||||
.Select(gr => gr.Key)
|
|
||||||
.ToDictionary(x => x);
|
|
||||||
|
|
||||||
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
|
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
|
||||||
{
|
{
|
||||||
using var media = new Media(_libVLC, x.FullName);
|
using var media = new Media(_libVLC, x.FullName);
|
||||||
@@ -390,24 +373,33 @@ public partial class Annotator
|
|||||||
{
|
{
|
||||||
Name = x.Name,
|
Name = x.Name,
|
||||||
Path = x.FullName,
|
Path = x.FullName,
|
||||||
MediaType = MediaTypes.Video,
|
MediaType = MediaTypes.Video
|
||||||
HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", ""))
|
|
||||||
};
|
};
|
||||||
media.ParsedChanged += (_, _) => fInfo.Duration = TimeSpan.FromMilliseconds(media.Duration);
|
media.ParsedChanged += (_, _) => fInfo.Duration = TimeSpan.FromMilliseconds(media.Duration);
|
||||||
return fInfo;
|
return fInfo;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray()).Select(x => new MediaFileInfo
|
var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
|
||||||
{
|
.Select(x => new MediaFileInfo
|
||||||
Name = x.Name,
|
{
|
||||||
Path = x.FullName,
|
Name = x.Name,
|
||||||
MediaType = MediaTypes.Image,
|
Path = x.FullName,
|
||||||
HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", ""))
|
MediaType = MediaTypes.Image
|
||||||
});
|
});
|
||||||
|
var allFiles = videoFiles.Concat(imageFiles).ToList();
|
||||||
|
|
||||||
AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList());
|
var allFileNames = allFiles.Select(x => x.FName).ToList();
|
||||||
|
|
||||||
|
var labelsDict = await _dbFactory.Run(async db => await db.Annotations
|
||||||
|
.GroupBy(x => x.Name.Substring(0, x.Name.Length - 7))
|
||||||
|
.Where(x => allFileNames.Contains(x.Key))
|
||||||
|
.ToDictionaryAsync(x => x.Key, x => x.Key));
|
||||||
|
|
||||||
|
foreach (var mediaFile in allFiles)
|
||||||
|
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
|
||||||
|
|
||||||
|
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
|
||||||
LvFiles.ItemsSource = AllMediaFiles;
|
LvFiles.ItemsSource = AllMediaFiles;
|
||||||
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
|
|
||||||
|
|
||||||
BlinkHelp(AllMediaFiles.Count == 0
|
BlinkHelp(AllMediaFiles.Count == 0
|
||||||
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
||||||
@@ -417,6 +409,10 @@ public partial class Annotator
|
|||||||
|
|
||||||
private void OnFormClosed(object? sender, EventArgs e)
|
private void OnFormClosed(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
MainCancellationSource.Cancel();
|
||||||
|
_inferenceService.StopInference();
|
||||||
|
DetectionCancellationSource.Cancel();
|
||||||
|
|
||||||
_mediaPlayer.Stop();
|
_mediaPlayer.Stop();
|
||||||
_mediaPlayer.Dispose();
|
_mediaPlayer.Dispose();
|
||||||
_libVLC.Dispose();
|
_libVLC.Dispose();
|
||||||
@@ -431,22 +427,18 @@ public partial class Annotator
|
|||||||
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
|
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SeekTo(long timeMilliseconds)
|
public void SeekTo(long timeMilliseconds, bool setPause = true)
|
||||||
{
|
{
|
||||||
_mediaPlayer.SetPause(true);
|
_mediaPlayer.SetPause(setPause);
|
||||||
_mediaPlayer.Time = timeMilliseconds;
|
_mediaPlayer.Time = timeMilliseconds;
|
||||||
VideoSlider.Value = _mediaPlayer.Position * 100;
|
VideoSlider.Value = _mediaPlayer.Position * 100;
|
||||||
|
|
||||||
|
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SeekTo(TimeSpan time) =>
|
private void SeekTo(TimeSpan time) =>
|
||||||
SeekTo((long)time.TotalMilliseconds);
|
SeekTo((long)time.TotalMilliseconds);
|
||||||
|
|
||||||
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
|
|
||||||
// {
|
|
||||||
// LvClasses.IsReadOnly = false;
|
|
||||||
// DetectionClasses.Add(new DetectionClass(DetectionClasses.Count));
|
|
||||||
// LvClasses.SelectedIndex = DetectionClasses.Count - 1;
|
|
||||||
// }
|
|
||||||
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
||||||
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
|
||||||
|
|
||||||
@@ -458,41 +450,42 @@ public partial class Annotator
|
|||||||
IsFolderPicker = true,
|
IsFolderPicker = true,
|
||||||
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
|
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
|
||||||
};
|
};
|
||||||
if (dlg.ShowDialog() != CommonFileDialogResult.Ok)
|
var dialogResult = dlg.ShowDialog();
|
||||||
|
|
||||||
|
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(dlg.FileName))
|
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
||||||
{
|
TbFolder.Text = dlg.FileName;
|
||||||
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
|
await Task.CompletedTask;
|
||||||
await SaveUserSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
ReloadFiles();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
|
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
|
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
|
||||||
LvFiles.ItemsSource = FilteredMediaFiles;
|
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||||
|
LvFiles.ItemsSource = FilteredMediaFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlayClick(object sender, RoutedEventArgs e)
|
private void PlayClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_mediator.Publish(new PlaybackControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
|
if (IsInferenceNow)
|
||||||
|
FollowAI = false;
|
||||||
|
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Pause));
|
private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Pause));
|
||||||
private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Stop));
|
private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Stop));
|
||||||
|
|
||||||
private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.PreviousFrame));
|
private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.PreviousFrame));
|
||||||
private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.NextFrame));
|
private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.NextFrame));
|
||||||
|
|
||||||
private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.SaveAnnotations));
|
private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.SaveAnnotations));
|
||||||
|
|
||||||
private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveSelectedAnns));
|
private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveSelectedAnns));
|
||||||
private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveAllAnns));
|
private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveAllAnns));
|
||||||
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOffVolume));
|
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOffVolume));
|
||||||
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOnVolume));
|
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOnVolume));
|
||||||
|
|
||||||
private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
|
private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -508,215 +501,106 @@ public partial class Annotator
|
|||||||
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
LvFilesContextMenu.DataContext = listItem!.DataContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
private (TimeSpan Time, List<Detection> Detections)? _previousDetection;
|
public void AutoDetect(object sender, RoutedEventArgs e)
|
||||||
|
|
||||||
public async void AutoDetect(object sender, RoutedEventArgs e)
|
|
||||||
{
|
{
|
||||||
|
if (IsInferenceNow)
|
||||||
|
{
|
||||||
|
FollowAI = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (LvFiles.Items.IsEmpty)
|
if (LvFiles.Items.IsEmpty)
|
||||||
return;
|
return;
|
||||||
if (LvFiles.SelectedIndex == -1)
|
if (LvFiles.SelectedIndex == -1)
|
||||||
LvFiles.SelectedIndex = 0;
|
LvFiles.SelectedIndex = 0;
|
||||||
|
|
||||||
await _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play));
|
Dispatcher.Invoke(() => Editor.ResetBackground());
|
||||||
_mediaPlayer.SetPause(true);
|
|
||||||
|
|
||||||
var manualCancellationSource = new CancellationTokenSource();
|
|
||||||
var token = manualCancellationSource.Token;
|
|
||||||
|
|
||||||
_autoDetectDialog = new AutodetectDialog
|
|
||||||
{
|
|
||||||
Topmost = true,
|
|
||||||
Owner = this
|
|
||||||
};
|
|
||||||
_autoDetectDialog.Closing += (_, _) =>
|
|
||||||
{
|
|
||||||
manualCancellationSource.Cancel();
|
|
||||||
_mediaPlayer.SeekTo(TimeSpan.Zero);
|
|
||||||
Editor.RemoveAllAnns();
|
|
||||||
};
|
|
||||||
|
|
||||||
_autoDetectDialog.Top = Height - _autoDetectDialog.Height - 80;
|
|
||||||
_autoDetectDialog.Left = 5;
|
|
||||||
|
|
||||||
_autoDetectDialog.Log("Ініціалізація AI...");
|
|
||||||
|
|
||||||
|
IsInferenceNow = true;
|
||||||
|
FollowAI = true;
|
||||||
|
DetectionCancellationSource = new CancellationTokenSource();
|
||||||
|
var detectToken = DetectionCancellationSource.Token;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var mediaInfo = Dispatcher.Invoke(() => (MediaFileInfo)LvFiles.SelectedItem);
|
while (!detectToken.IsCancellationRequested)
|
||||||
while (mediaInfo != null)
|
|
||||||
{
|
{
|
||||||
_formState.CurrentMedia = mediaInfo;
|
var files = new List<string>();
|
||||||
await Dispatcher.Invoke(async () => await ReloadAnnotations(token));
|
await Dispatcher.Invoke(async () =>
|
||||||
|
|
||||||
if (mediaInfo.MediaType == MediaTypes.Image)
|
|
||||||
{
|
{
|
||||||
await DetectImage(mediaInfo, manualCancellationSource, token);
|
//Take all medias
|
||||||
await Task.Delay(70, token);
|
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
|
||||||
}
|
//.Where(x => !x.HasAnnotations)
|
||||||
else
|
.Take(Constants.DETECTION_BATCH_SIZE)
|
||||||
await DetectVideo(mediaInfo, manualCancellationSource, token);
|
.Select(x => x.Path)
|
||||||
|
.ToList() ?? [];
|
||||||
|
if (files.Count != 0)
|
||||||
|
{
|
||||||
|
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
|
||||||
|
await ReloadAnnotations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (files.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
mediaInfo = Dispatcher.Invoke(() =>
|
await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken);
|
||||||
|
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
if (LvFiles.SelectedIndex == LvFiles.Items.Count - 1)
|
if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
|
||||||
return null;
|
DetectionCancellationSource.Cancel();
|
||||||
LvFiles.SelectedIndex += 1;
|
LvFiles.SelectedIndex += files.Count;
|
||||||
return (MediaFileInfo)LvFiles.SelectedItem;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
_autoDetectDialog.Close();
|
|
||||||
_mediaPlayer.Stop();
|
|
||||||
LvFiles.Items.Refresh();
|
LvFiles.Items.Refresh();
|
||||||
|
IsInferenceNow = false;
|
||||||
|
FollowAI = false;
|
||||||
});
|
});
|
||||||
}, token);
|
});
|
||||||
|
|
||||||
_autoDetectDialog.ShowDialog();
|
|
||||||
Dispatcher.Invoke(() => Editor.ResetBackground());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DetectImage(MediaFileInfo mediaInfo, CancellationTokenSource manualCancellationSource, CancellationToken token)
|
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
var fName = Path.GetFileNameWithoutExtension(mediaInfo.Path);
|
|
||||||
var stream = new FileStream(mediaInfo.Path, FileMode.Open);
|
|
||||||
var detections = await _aiDetector.Detect(fName, stream, token);
|
|
||||||
await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), Path.GetExtension(mediaInfo.Path), detections, token);
|
|
||||||
if (detections.Count != 0)
|
|
||||||
mediaInfo.HasAnnotations = true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, e.Message);
|
|
||||||
await manualCancellationSource.CancelAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DetectVideo(MediaFileInfo mediaInfo, CancellationTokenSource manualCancellationSource, CancellationToken token)
|
|
||||||
{
|
|
||||||
var prevSeekTime = 0.0;
|
|
||||||
await foreach (var timeframe in _vlcFrameExtractor.ExtractFrames(mediaInfo.Path, token))
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Detect time: {timeframe.Time}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fName = _formState.GetTimeName(timeframe.Time);
|
|
||||||
var detections = await _aiDetector.Detect(fName, timeframe.Stream, token);
|
|
||||||
var isValid = IsValidDetection(timeframe.Time, detections);
|
|
||||||
|
|
||||||
if (timeframe.Time.TotalSeconds > prevSeekTime + 1)
|
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() => SeekTo(timeframe.Time));
|
|
||||||
prevSeekTime = timeframe.Time.TotalSeconds;
|
|
||||||
if (!isValid) //Show frame anyway
|
|
||||||
{
|
|
||||||
var bitmap = new BitmapImage();
|
|
||||||
bitmap.BeginInit();
|
|
||||||
timeframe.Stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
bitmap.StreamSource = timeframe.Stream;
|
|
||||||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
|
||||||
bitmap.EndInit();
|
|
||||||
bitmap.Freeze();
|
|
||||||
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
Editor.RemoveAllAnns();
|
|
||||||
Editor.Background = new ImageBrush { ImageSource = bitmap };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
mediaInfo.HasAnnotations = true;
|
|
||||||
await ProcessDetection(timeframe, "jpg", detections, token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, ex.Message);
|
|
||||||
await manualCancellationSource.CancelAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsValidDetection(TimeSpan time, List<Detection> detections)
|
|
||||||
{
|
|
||||||
// No AI detection, forbid
|
|
||||||
if (detections.Count == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Very first detection, allow
|
|
||||||
if (!_previousDetection.HasValue)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var prev = _previousDetection.Value;
|
|
||||||
|
|
||||||
// Time between detections is >= than Frame Recognition Seconds, allow
|
|
||||||
if (time >= prev.Time.Add(TimeSpan.FromSeconds(_appConfig.AIRecognitionConfig.FrameRecognitionSeconds)))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Detection is earlier than previous + FrameRecognitionSeconds.
|
|
||||||
// Look to the detections more in detail
|
|
||||||
|
|
||||||
// More detected objects, allow
|
|
||||||
if (detections.Count > prev.Detections.Count)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
foreach (var det in detections)
|
|
||||||
{
|
|
||||||
var point = new Point(det.CenterX, det.CenterY);
|
|
||||||
var closestObject = prev.Detections
|
|
||||||
.Select(p => new
|
|
||||||
{
|
|
||||||
Point = p,
|
|
||||||
Distance = point.SqrDistance(new Point(p.CenterX, p.CenterY))
|
|
||||||
})
|
|
||||||
.OrderBy(x => x.Distance)
|
|
||||||
.First();
|
|
||||||
|
|
||||||
// Closest object is farther than Tracking distance confidence, hence it's a different object, allow
|
|
||||||
if (closestObject.Distance > _appConfig.AIRecognitionConfig.TrackingDistanceConfidence)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Since closest object within distance confidence, then it is tracking of the same object. Then if recognition probability for the object > increase from previous
|
|
||||||
if (det.Probability >= closestObject.Point.Probability + _appConfig.AIRecognitionConfig.TrackingProbabilityIncrease)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, string imageExtension, List<Detection> detections, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
_previousDetection = (timeframe.Time, detections);
|
|
||||||
await Dispatcher.Invoke(async () =>
|
await Dispatcher.Invoke(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var time = timeframe.Time;
|
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
|
||||||
|
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
|
||||||
|
{
|
||||||
|
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
|
||||||
|
.Select((info, i) => new
|
||||||
|
{
|
||||||
|
MediaInfo = info,
|
||||||
|
Index = i
|
||||||
|
})
|
||||||
|
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
|
||||||
|
if (nextFile != null)
|
||||||
|
{
|
||||||
|
LvFiles.SelectedIndex = nextFile.Index;
|
||||||
|
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var fName = _formState.GetTimeName(timeframe.Time);
|
AddAnnotation(annotation);
|
||||||
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.{imageExtension}");
|
|
||||||
|
|
||||||
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() };
|
if (FollowAI)
|
||||||
Editor.RemoveAllAnns();
|
SeekTo(annotationImage.Milliseconds, false);
|
||||||
AddAnnotationsToCanvas(time, detections, true);
|
|
||||||
await AddAnnotations(fName, detections, token);
|
|
||||||
|
|
||||||
await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token);
|
var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
|
||||||
|
|
||||||
var log = string.Join(Environment.NewLine, detections.Select(det =>
|
|
||||||
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
|
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
|
||||||
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
|
||||||
$"size=({det.Width:F2}, {det.Height:F2}), " +
|
$"size=({det.Width:F2}, {det.Height:F2}), " +
|
||||||
$"prob: {det.Probability:F1}%"));
|
$"conf: {det.Confidence*100:F0}%"));
|
||||||
|
|
||||||
Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
|
|
||||||
|
|
||||||
|
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (_formState.CurrentMedia != null)
|
||||||
|
_formState.CurrentMedia.HasAnnotations = true;
|
||||||
|
LvFiles.Items.Refresh();
|
||||||
|
StatusHelp.Text = log;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -724,4 +608,70 @@ public partial class Annotator
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_gpsPanelVisible = !_gpsPanelVisible;
|
||||||
|
|
||||||
|
if (_gpsPanelVisible)
|
||||||
|
{
|
||||||
|
GpsSplitterRow.Height = new GridLength(4);
|
||||||
|
GpsSplitter.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
GpsSectionRow.Height = new GridLength(1, GridUnitType.Star);
|
||||||
|
MapMatcherComponent.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GpsSplitterRow.Height = new GridLength(0);
|
||||||
|
GpsSplitter.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
GpsSectionRow.Height = new GridLength(0);
|
||||||
|
MapMatcherComponent.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SoundDetections(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GradientStyleSelector : StyleSelector
|
||||||
|
{
|
||||||
|
public override Style? SelectStyle(object item, DependencyObject container)
|
||||||
|
{
|
||||||
|
if (container is not DataGridRow row || row.DataContext is not AnnotationResult result)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var style = new Style(typeof(DataGridRow));
|
||||||
|
var brush = new LinearGradientBrush
|
||||||
|
{
|
||||||
|
StartPoint = new Point(0, 0),
|
||||||
|
EndPoint = new Point(1, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
var gradients = new List<GradientStop>();
|
||||||
|
if (result.Colors.Count == 0)
|
||||||
|
{
|
||||||
|
var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD");
|
||||||
|
gradients = [new GradientStop(color, 0.99)];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var increment = 1.0 / result.Colors.Count;
|
||||||
|
var currentStop = increment;
|
||||||
|
foreach (var c in result.Colors)
|
||||||
|
{
|
||||||
|
var resultColor = c.Color.ToConfidenceColor(c.Confidence);
|
||||||
|
brush.GradientStops.Add(new GradientStop(resultColor, currentStop));
|
||||||
|
currentStop += increment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var gradientStop in gradients)
|
||||||
|
brush.GradientStops.Add(gradientStop);
|
||||||
|
|
||||||
|
style.Setters.Add(new Setter(DataGridRow.BackgroundProperty, brush));
|
||||||
|
return style;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ using System.Windows.Input;
|
|||||||
using Azaion.Annotator.DTO;
|
using Azaion.Annotator.DTO;
|
||||||
using Azaion.Common;
|
using Azaion.Common;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
using LibVLCSharp.Shared;
|
using LibVLCSharp.Shared;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -22,12 +23,14 @@ public class AnnotatorEventHandler(
|
|||||||
FormState formState,
|
FormState formState,
|
||||||
AnnotationService annotationService,
|
AnnotationService annotationService,
|
||||||
ILogger<AnnotatorEventHandler> logger,
|
ILogger<AnnotatorEventHandler> logger,
|
||||||
IOptions<DirectoriesConfig> dirConfig)
|
IOptions<DirectoriesConfig> dirConfig,
|
||||||
|
IInferenceService inferenceService)
|
||||||
:
|
:
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<AnnClassSelectedEvent>,
|
INotificationHandler<AnnClassSelectedEvent>,
|
||||||
INotificationHandler<PlaybackControlEvent>,
|
INotificationHandler<AnnotatorControlEvent>,
|
||||||
INotificationHandler<VolumeChangedEvent>
|
INotificationHandler<VolumeChangedEvent>,
|
||||||
|
INotificationHandler<AnnotationsDeletedEvent>
|
||||||
{
|
{
|
||||||
private const int STEP = 20;
|
private const int STEP = 20;
|
||||||
private const int LARGE_STEP = 5000;
|
private const int LARGE_STEP = 5000;
|
||||||
@@ -51,12 +54,12 @@ public class AnnotatorEventHandler(
|
|||||||
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 cancellationToken = default)
|
||||||
@@ -72,12 +75,12 @@ 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, cancellationToken);
|
||||||
|
|
||||||
if (key == Key.A)
|
if (key == Key.R)
|
||||||
mainWindow.AutoDetect(null!, null!);
|
mainWindow.AutoDetect(null!, null!);
|
||||||
|
|
||||||
#region Volume
|
#region Volume
|
||||||
@@ -105,7 +108,7 @@ public class AnnotatorEventHandler(
|
|||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken = default)
|
public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ControlPlayback(notification.PlaybackControl, cancellationToken);
|
await ControlPlayback(notification.PlaybackControl, cancellationToken);
|
||||||
mainWindow.VideoView.Focus();
|
mainWindow.VideoView.Focus();
|
||||||
@@ -121,12 +124,15 @@ 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:
|
||||||
mediaPlayer.Pause();
|
mediaPlayer.Pause();
|
||||||
|
if (mainWindow.IsInferenceNow)
|
||||||
|
mainWindow.FollowAI = false;
|
||||||
if (!mediaPlayer.IsPlaying)
|
if (!mediaPlayer.IsPlaying)
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
|
||||||
|
|
||||||
if (formState.BackgroundTime.HasValue)
|
if (formState.BackgroundTime.HasValue)
|
||||||
{
|
{
|
||||||
mainWindow.Editor.ResetBackground();
|
mainWindow.Editor.ResetBackground();
|
||||||
@@ -134,6 +140,8 @@ public class AnnotatorEventHandler(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.Stop:
|
case PlaybackControlEnum.Stop:
|
||||||
|
inferenceService.StopInference();
|
||||||
|
await mainWindow.DetectionCancellationSource.CancelAsync();
|
||||||
mediaPlayer.Stop();
|
mediaPlayer.Stop();
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.PreviousFrame:
|
case PlaybackControlEnum.PreviousFrame:
|
||||||
@@ -164,10 +172,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 +190,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,7 +198,7 @@ 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 cancellationToken)
|
||||||
@@ -205,7 +213,7 @@ 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;
|
||||||
@@ -213,10 +221,14 @@ public class AnnotatorEventHandler(
|
|||||||
mainWindow.Editor.ResetBackground();
|
mainWindow.Editor.ResetBackground();
|
||||||
|
|
||||||
formState.CurrentMedia = mediaInfo;
|
formState.CurrentMedia = mediaInfo;
|
||||||
|
//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}";
|
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
|
||||||
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
|
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
|
||||||
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
|
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
|
||||||
|
if (formState.CurrentMedia.MediaType == MediaTypes.Image)
|
||||||
|
mediaPlayer.SetPause(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//SAVE: MANUAL
|
//SAVE: MANUAL
|
||||||
@@ -226,47 +238,67 @@ public class AnnotatorEventHandler(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
|
||||||
var fName = formState.GetTimeName(time);
|
var originalMediaName = formState.VideoName;
|
||||||
|
var fName = originalMediaName.ToTimeName(time);
|
||||||
|
|
||||||
var currentDetections = mainWindow.Editor.CurrentDetections
|
var currentDetections = mainWindow.Editor.CurrentDetections
|
||||||
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
|
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken);
|
formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
|
||||||
|
|
||||||
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
|
|
||||||
mainWindow.LvFiles.Items.Refresh();
|
mainWindow.LvFiles.Items.Refresh();
|
||||||
mainWindow.Editor.RemoveAllAnns();
|
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, $"{fName}{Constants.JPG_EXT}");
|
||||||
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}");
|
|
||||||
|
|
||||||
if (isVideo)
|
if (formState.BackgroundTime.HasValue)
|
||||||
{
|
{
|
||||||
if (formState.BackgroundTime.HasValue)
|
//no need to save image, it's already there, just remove background
|
||||||
{
|
mainWindow.Editor.ResetBackground();
|
||||||
//no need to save image, it's already there, just remove background
|
formState.BackgroundTime = null;
|
||||||
mainWindow.Editor.ResetBackground();
|
|
||||||
formState.BackgroundTime = null;
|
|
||||||
|
|
||||||
//next item
|
//next item
|
||||||
var annGrid = mainWindow.DgAnnotations;
|
var annGrid = mainWindow.DgAnnotations;
|
||||||
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
|
||||||
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
|
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
|
||||||
NextMedia();
|
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
|
||||||
|
if (isVideo)
|
||||||
|
mediaPlayer.Play();
|
||||||
|
else
|
||||||
|
await NextMedia(ct: cancellationToken);
|
||||||
}
|
}
|
||||||
await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
|
|
||||||
|
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
|
||||||
|
if (isVideo)
|
||||||
|
mainWindow.AddAnnotation(annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x);
|
||||||
|
foreach (var ann in notification.Annotations)
|
||||||
|
{
|
||||||
|
if (!annResDict.TryGetValue(ann.Name, out var value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
formState.AnnotationResults.Remove(value);
|
||||||
|
mainWindow.TimedAnnotations.Remove(ann);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
</PropertyGroup>
|
</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.4.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||||
@@ -25,9 +26,10 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" 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, Brush background)
|
||||||
|
{
|
||||||
|
ShadowEffect = new DropShadowEffect();
|
||||||
|
Marker = m;
|
||||||
|
Marker.ZIndex = 100;
|
||||||
|
|
||||||
|
SizeChanged += CircleVisual_SizeChanged;
|
||||||
|
MouseEnter += CircleVisual_MouseEnter;
|
||||||
|
MouseLeave += CircleVisual_MouseLeave;
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
|
||||||
|
Text = "?";
|
||||||
|
|
||||||
|
StrokeArrow.EndLineCap = PenLineCap.Triangle;
|
||||||
|
StrokeArrow.LineJoin = PenLineJoin.Round;
|
||||||
|
|
||||||
|
RenderTransform = _scale;
|
||||||
|
|
||||||
|
Width = Height = 22;
|
||||||
|
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"/>
|
||||||
|
|
||||||
|
<controls:CanvasEditor
|
||||||
|
Grid.Column="2"
|
||||||
|
x:Name="GpsImageEditor"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
HorizontalAlignment="Stretch" >
|
||||||
|
</controls:CanvasEditor>
|
||||||
|
|
||||||
|
<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,160 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common;
|
||||||
|
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 GMap.NET.WindowsPresentation;
|
||||||
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.Controls;
|
||||||
|
|
||||||
|
public partial class MapMatcher : UserControl
|
||||||
|
{
|
||||||
|
private AppConfig _appConfig = null!;
|
||||||
|
List<MediaFileInfo> _allMediaFiles = new();
|
||||||
|
private 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 initialLat = double.Parse(TbLat.Text);
|
||||||
|
var initialLon = double.Parse(TbLon.Text);
|
||||||
|
|
||||||
|
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task SetMarker(GpsMatchResult result)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude));
|
||||||
|
var ann = _annotations[result.Index];
|
||||||
|
marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue)
|
||||||
|
{
|
||||||
|
Text = ann.Name
|
||||||
|
};
|
||||||
|
SatelliteMap.Markers.Add(marker);
|
||||||
|
ann.Lat = result.Latitude;
|
||||||
|
ann.Lon = result.Longitude;
|
||||||
|
SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude);
|
||||||
|
SatelliteMap.ZoomAndCenterMarkers(null);
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetFromCsv(List<MediaFileInfo> mediaFiles)
|
||||||
|
{
|
||||||
|
|
||||||
|
var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH);
|
||||||
|
var csvDict = csvResults
|
||||||
|
.Where(x => x.MatchType == "stitched")
|
||||||
|
.ToDictionary(x => x.Index);
|
||||||
|
foreach (var ann in _annotations)
|
||||||
|
{
|
||||||
|
var csvRes = csvDict.GetValueOrDefault(ann.Key);
|
||||||
|
if (csvRes == null)
|
||||||
|
continue;
|
||||||
|
await SetMarker(csvRes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TestGps(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var initialLat = double.Parse(TbLat.Text);
|
||||||
|
var initialLon = double.Parse(TbLon.Text);
|
||||||
|
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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();
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,21 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
<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.4.1" />
|
||||||
<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.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="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="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
|
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
|
||||||
|
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+58
-35
@@ -1,11 +1,15 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
namespace Azaion.Common;
|
namespace Azaion.Common;
|
||||||
|
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public const string SECURE_RESOURCE_CACHE = "SecureResourceCache";
|
public const string JPG_EXT = ".jpg";
|
||||||
|
|
||||||
#region DirectoriesConfig
|
#region DirectoriesConfig
|
||||||
|
|
||||||
@@ -14,25 +18,39 @@ public class Constants
|
|||||||
public const string DEFAULT_IMAGES_DIR = "images";
|
public const string DEFAULT_IMAGES_DIR = "images";
|
||||||
public const string DEFAULT_RESULTS_DIR = "results";
|
public const string DEFAULT_RESULTS_DIR = "results";
|
||||||
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
|
||||||
|
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
|
||||||
|
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region AnnotatorConfig
|
#region AnnotatorConfig
|
||||||
|
|
||||||
public static readonly List<DetectionClass> DefaultAnnotationClasses =
|
public static readonly AnnotationConfig DefaultAnnotationConfig = new()
|
||||||
|
{
|
||||||
|
DetectionClasses = DefaultAnnotationClasses!,
|
||||||
|
VideoFormats = DefaultVideoFormats!,
|
||||||
|
ImageFormats = DefaultImageFormats!,
|
||||||
|
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly List<DetectionClass> DefaultAnnotationClasses =
|
||||||
[
|
[
|
||||||
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" },
|
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
|
||||||
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" },
|
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
|
||||||
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" },
|
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
|
||||||
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" },
|
new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
|
||||||
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" },
|
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
|
||||||
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" },
|
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
|
||||||
new() { Id = 6, Name = "Військовий", ShortName = "Військов" },
|
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
|
||||||
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
|
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
|
||||||
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
|
new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() },
|
||||||
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
|
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
|
||||||
new() { Id = 10, Name = "Літак", ShortName = "Літак" },
|
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
|
||||||
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" }
|
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
|
||||||
|
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
|
||||||
|
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
|
||||||
|
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
|
||||||
|
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
||||||
@@ -47,49 +65,45 @@ public class Constants
|
|||||||
|
|
||||||
# region AIRecognitionConfig
|
# region AIRecognitionConfig
|
||||||
|
|
||||||
|
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
|
||||||
|
{
|
||||||
|
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
|
||||||
|
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
|
||||||
|
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
|
||||||
|
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
|
||||||
|
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
|
||||||
|
};
|
||||||
|
|
||||||
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
|
||||||
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
|
||||||
public const double TRACKING_PROBABILITY_INCREASE = 15;
|
public const double TRACKING_PROBABILITY_INCREASE = 15;
|
||||||
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
|
||||||
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
|
||||||
|
|
||||||
|
public const int DETECTION_BATCH_SIZE = 4;
|
||||||
# endregion AIRecognitionConfig
|
# endregion AIRecognitionConfig
|
||||||
|
|
||||||
#region Thumbnails
|
#region Thumbnails
|
||||||
|
|
||||||
|
public static readonly ThumbnailConfig DefaultThumbnailConfig = new()
|
||||||
|
{
|
||||||
|
Size = DefaultThumbnailSize,
|
||||||
|
Border = DEFAULT_THUMBNAIL_BORDER
|
||||||
|
};
|
||||||
|
|
||||||
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||||
|
|
||||||
public const int DEFAULT_THUMBNAIL_BORDER = 10;
|
public const int DEFAULT_THUMBNAIL_BORDER = 10;
|
||||||
|
|
||||||
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
|
#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 MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
|
||||||
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
|
|
||||||
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -102,4 +116,13 @@ public class Constants
|
|||||||
|
|
||||||
#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 CSV_PATH = "matches.csv";
|
||||||
}
|
}
|
||||||
@@ -34,13 +34,13 @@ public class CanvasEditor : Canvas
|
|||||||
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 +54,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 +84,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,
|
||||||
@@ -154,7 +154,25 @@ public class CanvasEditor : Canvas
|
|||||||
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (SelectionState == SelectionState.NewAnnCreating)
|
if (SelectionState == SelectionState.NewAnnCreating)
|
||||||
CreateAnnotation(e.GetPosition(this));
|
{
|
||||||
|
var endPos = e.GetPosition(this);
|
||||||
|
_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)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var time = GetTimeFunc();
|
||||||
|
CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
||||||
|
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
|
||||||
|
Confidence = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
SelectionState = SelectionState.None;
|
SelectionState = SelectionState.None;
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
@@ -291,39 +309,25 @@ public class CanvasEditor : Canvas
|
|||||||
SetTop(_newAnnotationRect, currentPos.Y);
|
SetTop(_newAnnotationRect, currentPos.Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateAnnotation(Point endPos)
|
public void CreateDetections(TimeSpan time, IEnumerable<Detection> detections, List<DetectionClass> detectionClasses, Size videoSize)
|
||||||
{
|
{
|
||||||
_newAnnotationRect.Width = 0;
|
foreach (var detection in detections)
|
||||||
_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)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var time = GetTimeFunc();
|
|
||||||
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
|
|
||||||
{
|
{
|
||||||
Width = width,
|
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
|
||||||
Height = height,
|
var canvasLabel = new CanvasLabel(detection, RenderSize, videoSize, detection.Confidence);
|
||||||
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
|
CreateDetectionControl(detectionClass, time, canvasLabel);
|
||||||
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel)
|
private void 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.X );
|
||||||
Height = canvasLabel.Height
|
SetTop(detectionControl, canvasLabel.Y);
|
||||||
};
|
Children.Add(detectionControl);
|
||||||
annotationControl.MouseDown += AnnotationPositionStart;
|
CurrentDetections.Add(detectionControl);
|
||||||
SetLeft(annotationControl, canvasLabel.X );
|
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
|
||||||
SetTop(annotationControl, canvasLabel.Y);
|
|
||||||
Children.Add(annotationControl);
|
|
||||||
CurrentDetections.Add(annotationControl);
|
|
||||||
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
|
|
||||||
return annotationControl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -355,8 +359,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,183 @@
|
|||||||
<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">
|
||||||
Background="Black"
|
<UserControl.Resources>
|
||||||
RowBackground="#252525"
|
<Style x:Key="ButtonRadioButtonStyle" TargetType="RadioButton">
|
||||||
Foreground="White"
|
<Setter Property="Template">
|
||||||
RowHeaderWidth="0"
|
<Setter.Value>
|
||||||
Padding="2 0 0 0"
|
<ControlTemplate TargetType="RadioButton">
|
||||||
AutoGenerateColumns="False"
|
<Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
SelectionMode="Single"
|
Background="{TemplateBinding Background}" BorderThickness="1"
|
||||||
CellStyle="{DynamicResource DataGridCellStyle1}"
|
Padding="10,5" CornerRadius="2">
|
||||||
IsReadOnly="True"
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
CanUserResizeRows="False"
|
</Border>
|
||||||
CanUserResizeColumns="False">
|
<ControlTemplate.Triggers>
|
||||||
<DataGrid.Columns>
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<DataGridTemplateColumn
|
<Setter TargetName="Border" Property="Background" Value="Gray"/>
|
||||||
Width="50"
|
</Trigger>
|
||||||
Header="Клавіша"
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
CanUserSort="False">
|
<Setter TargetName="Border" Property="Background" Value="DarkGray"/>
|
||||||
<DataGridTemplateColumn.HeaderStyle>
|
</Trigger>
|
||||||
<Style TargetType="DataGridColumnHeader">
|
</ControlTemplate.Triggers>
|
||||||
<Setter Property="Background" Value="#252525"></Setter>
|
</ControlTemplate>
|
||||||
</Style>
|
</Setter.Value>
|
||||||
</DataGridTemplateColumn.HeaderStyle>
|
</Setter>
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
<DataTemplate>
|
<Setter Property="BorderBrush" Value="White"/>
|
||||||
<Border Background="{Binding Path=ColorBrush}">
|
<Setter Property="Foreground" Value="White"/>
|
||||||
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock>
|
</Style>
|
||||||
</Border>
|
</UserControl.Resources>
|
||||||
</DataTemplate>
|
<Grid Background="Black">
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<!-- Your DataGrid with detection classes -->
|
||||||
|
<DataGrid x:Name="DetectionDataGrid"
|
||||||
|
Grid.Row="0"
|
||||||
|
Background="Black"
|
||||||
|
RowBackground="#252525"
|
||||||
|
Foreground="White"
|
||||||
|
RowHeaderWidth="0"
|
||||||
|
Padding="2 0 0 0"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
SelectionMode="Single"
|
||||||
|
CellStyle="{DynamicResource DataGridCellStyle1}"
|
||||||
|
IsReadOnly="True"
|
||||||
|
CanUserResizeRows="False"
|
||||||
|
CanUserResizeColumns="False"
|
||||||
|
SelectionChanged="DetectionDataGrid_SelectionChanged"
|
||||||
|
x:FieldModifier="public"
|
||||||
|
>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
|
||||||
|
<DataGridTemplateColumn.HeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#252525"/>
|
||||||
|
</Style>
|
||||||
|
</DataGridTemplateColumn.HeaderStyle>
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Background="{Binding Path=ColorBrush}">
|
||||||
|
<TextBlock Text="{Binding Path=ClassNumber}"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
<DataGridTextColumn
|
<DataGridTextColumn Width="*" Header="Назва" Binding="{Binding Path=ShortName}" CanUserSort="False">
|
||||||
Width="*"
|
<DataGridTextColumn.HeaderStyle>
|
||||||
Header="Назва"
|
<Style TargetType="DataGridColumnHeader">
|
||||||
Binding="{Binding Path=Name}"
|
<Setter Property="Background" Value="#252525"/>
|
||||||
CanUserSort="False">
|
</Style>
|
||||||
<DataGridTextColumn.HeaderStyle>
|
</DataGridTextColumn.HeaderStyle>
|
||||||
<Style TargetType="DataGridColumnHeader">
|
|
||||||
<Setter Property="Background" Value="#252525"></Setter>
|
|
||||||
</Style>
|
|
||||||
</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,89 @@
|
|||||||
namespace Azaion.Common.Controls;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ 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 Azaion.Common.Extensions;
|
||||||
using Label = System.Windows.Controls.Label;
|
using Label = System.Windows.Controls.Label;
|
||||||
|
|
||||||
namespace Azaion.Common.Controls;
|
namespace Azaion.Common.Controls;
|
||||||
@@ -11,12 +12,13 @@ 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 = 12;
|
||||||
|
|
||||||
private readonly Grid _grid;
|
private readonly Grid _grid;
|
||||||
private readonly TextBlock _classNameLabel;
|
private readonly Label _detectionLabel;
|
||||||
private readonly Label _probabilityLabel;
|
public TimeSpan Time { get; set; }
|
||||||
public TimeSpan? Time { get; set; }
|
private readonly double _confidence;
|
||||||
|
private List<Rectangle> _resizedRectangles = new();
|
||||||
|
|
||||||
private DetectionClass _detectionClass = null!;
|
private DetectionClass _detectionClass = null!;
|
||||||
public DetectionClass DetectionClass
|
public DetectionClass DetectionClass
|
||||||
@@ -24,9 +26,14 @@ 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(3);
|
||||||
|
foreach (var rect in _resizedRectangles)
|
||||||
|
rect.Stroke = brush;
|
||||||
|
|
||||||
|
_detectionLabel.Background = new SolidColorBrush(value.Color.ToConfidenceColor(_confidence));
|
||||||
|
_detectionLabel.Content = _detectionLabelText(value.UIName);
|
||||||
_detectionClass = value;
|
_detectionClass = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,28 +51,35 @@ public class DetectionControl : Border
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null)
|
private string _detectionLabelText(string detectionClassName) =>
|
||||||
|
_confidence >= 0.995 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%"; //double
|
||||||
|
|
||||||
|
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
|
_confidence = canvasLabel.Confidence;
|
||||||
|
|
||||||
|
var labelContainer = new Canvas
|
||||||
{
|
{
|
||||||
Text = detectionClass.Name,
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
HorizontalAlignment = HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
Margin = new Thickness(0, 15, 0, 0),
|
ClipToBounds = false,
|
||||||
FontSize = 14,
|
Margin = new Thickness(0, 0, 32, 0)
|
||||||
Cursor = Cursors.SizeAll
|
|
||||||
};
|
};
|
||||||
_probabilityLabel = new Label
|
_detectionLabel = new Label
|
||||||
{
|
{
|
||||||
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty,
|
Content = _detectionLabelText(detectionClass.Name),
|
||||||
HorizontalAlignment = HorizontalAlignment.Right,
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
VerticalAlignment = VerticalAlignment.Top,
|
VerticalAlignment = VerticalAlignment.Top,
|
||||||
Margin = new Thickness(0, -32, 0, 0),
|
Margin = new Thickness(0, -32, 0, 0),
|
||||||
FontSize = 16,
|
FontSize = 16,
|
||||||
Visibility = Visibility.Visible
|
Visibility = Visibility.Visible
|
||||||
};
|
};
|
||||||
|
labelContainer.Children.Add(_detectionLabel);
|
||||||
|
|
||||||
_selectionFrame = new Rectangle
|
_selectionFrame = new Rectangle
|
||||||
{
|
{
|
||||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
@@ -74,43 +88,46 @@ public class DetectionControl : Border
|
|||||||
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)
|
foreach (var rect in _resizedRectangles)
|
||||||
_grid.Children.Add(_probabilityLabel);
|
_grid.Children.Add(rect);
|
||||||
|
_grid.Children.Add(labelContainer);
|
||||||
|
|
||||||
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(-RESIZE_RECT_SIZE * 0.7),
|
||||||
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)),
|
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
|
||||||
Cursor = crs,
|
Cursor = crs,
|
||||||
Name = name,
|
Name = name,
|
||||||
};
|
};
|
||||||
@@ -120,7 +137,7 @@ public class DetectionControl : Border
|
|||||||
|
|
||||||
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
|
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
|
||||||
{
|
{
|
||||||
var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
var label = new CanvasLabel(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
|
||||||
return new YoloLabel(label, canvasSize, videoSize);
|
return new YoloLabel(label, canvasSize, videoSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,33 @@
|
|||||||
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!;
|
public List<(Color Color, double Confidence)> Colors { get; private set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "t")]
|
public string ImagePath { get; set; }
|
||||||
public TimeSpan Time { get; set; }
|
public string TimeStr { get; set; }
|
||||||
|
public string ClassName { get; set; }
|
||||||
|
|
||||||
public double Lat { get; set; }
|
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
|
||||||
public double Lon { get; set; }
|
{
|
||||||
public List<Detection> Detections { get; set; } = new();
|
|
||||||
|
|
||||||
#region For XAML Form
|
Annotation = annotation;
|
||||||
|
|
||||||
[JsonIgnore]
|
TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
|
||||||
public string TimeStr => $"{Time:h\\:mm\\:ss}";
|
ImagePath = annotation.ImagePath;
|
||||||
|
|
||||||
[JsonIgnore]
|
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
|
||||||
public string ClassName { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonIgnore]
|
Colors = annotation.Detections
|
||||||
public Color ClassColor0 { get; set; }
|
.Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
|
||||||
|
.ToList();
|
||||||
[JsonIgnore]
|
|
||||||
public Color ClassColor1 { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Color ClassColor2 { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public Color ClassColor3 { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
ClassName = detectionClasses.Count > 1
|
||||||
|
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
|
||||||
|
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+11
-9
@@ -2,11 +2,12 @@
|
|||||||
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) : INotifyPropertyChanged
|
||||||
{
|
{
|
||||||
public Annotation Annotation { get; set; } = annotation;
|
public Annotation Annotation { get; set; } = annotation;
|
||||||
|
|
||||||
@@ -19,20 +20,21 @@ 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
|
||||||
|
{
|
||||||
|
_thumbnail = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
|
||||||
|
|
||||||
public void Delete()
|
public string ImageName => Path.GetFileName(Annotation.ImagePath);
|
||||||
{
|
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
|
||||||
File.Delete(Annotation.ImagePath);
|
|
||||||
File.Delete(Annotation.LabelPath);
|
|
||||||
File.Delete(Annotation.ThumbPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
|
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; } = 2;
|
||||||
}
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
}
|
||||||
@@ -8,17 +8,23 @@ namespace Azaion.Common.DTO.Config;
|
|||||||
|
|
||||||
public class AppConfig
|
public class AppConfig
|
||||||
{
|
{
|
||||||
public ApiConfig ApiConfig { get; set; } = null!;
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
public QueueConfig QueueConfig { get; set; } = null!;
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
|
|
||||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
public QueueConfig QueueConfig { get; set; } = null!;
|
||||||
|
|
||||||
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||||
|
|
||||||
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
||||||
|
|
||||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
public UIConfig UIConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||||
|
|
||||||
|
public MapConfig MapConfig{ get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConfigUpdater
|
public interface IConfigUpdater
|
||||||
@@ -39,23 +45,13 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
|
|
||||||
var appConfig = new AppConfig
|
var appConfig = new AppConfig
|
||||||
{
|
{
|
||||||
ApiConfig = new ApiConfig
|
AnnotationConfig = Constants.DefaultAnnotationConfig,
|
||||||
{
|
|
||||||
Url = SecurityConstants.DEFAULT_API_URL,
|
|
||||||
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
|
|
||||||
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
|
||||||
},
|
|
||||||
|
|
||||||
AnnotationConfig = new AnnotationConfig
|
UIConfig = new UIConfig
|
||||||
{
|
{
|
||||||
AnnotationClasses = Constants.DefaultAnnotationClasses,
|
|
||||||
VideoFormats = Constants.DefaultVideoFormats,
|
|
||||||
ImageFormats = Constants.DefaultImageFormats,
|
|
||||||
|
|
||||||
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
||||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
||||||
|
GenerateAnnotatedImage = false
|
||||||
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
|
|
||||||
},
|
},
|
||||||
|
|
||||||
DirectoriesConfig = new DirectoriesConfig
|
DirectoriesConfig = new DirectoriesConfig
|
||||||
@@ -64,29 +60,28 @@ public class ConfigUpdater : IConfigUpdater
|
|||||||
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
|
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
|
||||||
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
|
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
|
||||||
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
|
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
|
||||||
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
|
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
|
||||||
|
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
|
||||||
|
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
|
||||||
},
|
},
|
||||||
|
|
||||||
ThumbnailConfig = new ThumbnailConfig
|
ThumbnailConfig = Constants.DefaultThumbnailConfig,
|
||||||
{
|
AIRecognitionConfig = Constants.DefaultAIRecognitionConfig
|
||||||
Size = Constants.DefaultThumbnailSize,
|
|
||||||
Border = Constants.DEFAULT_THUMBNAIL_BORDER
|
|
||||||
},
|
|
||||||
|
|
||||||
AIRecognitionConfig = new AIRecognitionConfig
|
|
||||||
{
|
|
||||||
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
|
|
||||||
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
|
|
||||||
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
|
|
||||||
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD,
|
|
||||||
FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Save(appConfig);
|
Save(appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(AppConfig config)
|
public void Save(AppConfig config)
|
||||||
{
|
{
|
||||||
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
//Save only user's config
|
||||||
|
var publicConfig = new
|
||||||
|
{
|
||||||
|
config.InferenceClientConfig,
|
||||||
|
config.GpsDeniedClientConfig,
|
||||||
|
config.DirectoriesConfig,
|
||||||
|
config.UIConfig
|
||||||
|
};
|
||||||
|
|
||||||
|
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -1,22 +1,61 @@
|
|||||||
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();
|
|
||||||
|
|
||||||
[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,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; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO;
|
namespace Azaion.Common.DTO;
|
||||||
@@ -7,9 +6,7 @@ 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 VideoName => CurrentMedia?.FName ?? "";
|
||||||
? ""
|
|
||||||
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
|
|
||||||
|
|
||||||
public string CurrentMrl { get; set; } = null!;
|
public string CurrentMrl { get; set; } = null!;
|
||||||
public Size CurrentVideoSize { get; set; }
|
public Size CurrentVideoSize { get; set; }
|
||||||
@@ -19,6 +16,4 @@ public class FormState
|
|||||||
public int CurrentVolume { get; set; } = 100;
|
public int CurrentVolume { get; set; } = 100;
|
||||||
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
|
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
|
||||||
public WindowEnum ActiveWindow { get; set; }
|
public WindowEnum ActiveWindow { get; set; }
|
||||||
|
|
||||||
public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
public class GpsMatchResult
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
public string Image { get; set; } = null!;
|
||||||
|
public double Latitude { get; set; }
|
||||||
|
public double Longitude { get; set; }
|
||||||
|
public int KeyPoints { get; set; }
|
||||||
|
public int Rotation { get; set; }
|
||||||
|
public string MatchType { get; set; } = null!;
|
||||||
|
|
||||||
|
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
|
||||||
|
{
|
||||||
|
var imageDatas = new List<GpsMatchResult>();
|
||||||
|
|
||||||
|
using var reader = new StreamReader(csvFilePath);
|
||||||
|
//read header
|
||||||
|
reader.ReadLine();
|
||||||
|
if (reader.EndOfStream)
|
||||||
|
return new List<GpsMatchResult>();
|
||||||
|
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = reader.ReadLine();
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
var values = line.Split(',');
|
||||||
|
if (values.Length == 6)
|
||||||
|
{
|
||||||
|
imageDatas.Add(new GpsMatchResult
|
||||||
|
{
|
||||||
|
Image = GetFilename(values[0]),
|
||||||
|
Latitude = double.Parse(values[1]),
|
||||||
|
Longitude = double.Parse(values[2]),
|
||||||
|
KeyPoints = int.Parse(values[3]),
|
||||||
|
Rotation = int.Parse(values[4]),
|
||||||
|
MatchType = values[5]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageDatas;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFilename(string imagePath) =>
|
||||||
|
Path.GetFileNameWithoutExtension(imagePath)
|
||||||
|
.Replace("-small", "");
|
||||||
|
}
|
||||||
+20
-18
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -26,22 +26,22 @@ public class CanvasLabel : Label
|
|||||||
public double Y { get; set; }
|
public double Y { 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 CanvasLabel()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber)
|
public CanvasLabel(int classNumber, double x, double y, double width, double height, double confidence = 1) : base(classNumber)
|
||||||
{
|
{
|
||||||
X = x;
|
X = x;
|
||||||
Y = y;
|
Y = y;
|
||||||
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? videoSize = null, double confidence = 1)
|
||||||
{
|
{
|
||||||
var cw = canvasSize.Width;
|
var cw = canvasSize.Width;
|
||||||
var ch = canvasSize.Height;
|
var ch = canvasSize.Height;
|
||||||
@@ -75,19 +75,20 @@ public class CanvasLabel : Label
|
|||||||
Width = label.Width * realWidth;
|
Width = label.Width * realWidth;
|
||||||
Height = label.Height * ch;
|
Height = label.Height * ch;
|
||||||
}
|
}
|
||||||
Probability = probability;
|
Confidence = 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()
|
||||||
{
|
{
|
||||||
@@ -184,15 +185,16 @@ public class YoloLabel : Label
|
|||||||
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
public class Detection : YoloLabel
|
public class Detection : YoloLabel
|
||||||
{
|
{
|
||||||
public string AnnotationName { get; set; }
|
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
|
||||||
public double? Probability { get; set; }
|
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
|
||||||
|
|
||||||
//For db
|
//For db & serialization
|
||||||
public Detection(){}
|
public Detection(){}
|
||||||
|
|
||||||
public Detection(string annotationName, YoloLabel label, double? probability = null)
|
public Detection(string annotationName, YoloLabel label, double confidence = 1)
|
||||||
{
|
{
|
||||||
AnnotationName = annotationName;
|
AnnotationName = annotationName;
|
||||||
ClassNumber = label.ClassNumber;
|
ClassNumber = label.ClassNumber;
|
||||||
@@ -200,6 +202,6 @@ public class Detection : YoloLabel
|
|||||||
CenterY = label.CenterY;
|
CenterY = label.CenterY;
|
||||||
Height = label.Height;
|
Height = label.Height;
|
||||||
Width = label.Width;
|
Width = label.Width;
|
||||||
Probability = probability;
|
Confidence = confidence;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,4 +1,5 @@
|
|||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
namespace Azaion.Common.DTO.Queue;
|
namespace Azaion.Common.DTO.Queue;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
@@ -6,15 +7,17 @@ using MessagePack;
|
|||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class AnnotationCreatedMessage
|
public class AnnotationCreatedMessage
|
||||||
{
|
{
|
||||||
[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 CreatedRole { get; set; }
|
||||||
[Key(8)] public AnnotationStatus Status { get; set; }
|
[Key(8)] public string CreatedEmail { get; set; } = null!;
|
||||||
|
[Key(9)] public SourceEnum Source { get; set; }
|
||||||
|
[Key(10)] public AnnotationStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.Common.DTO;
|
||||||
|
|
||||||
|
public class SatTile
|
||||||
|
{
|
||||||
|
public int X { get; }
|
||||||
|
public int Y { get; }
|
||||||
|
public double LeftTopLat { get; }
|
||||||
|
public double LeftTopLon { get; }
|
||||||
|
|
||||||
|
public double BottomRightLat { get; }
|
||||||
|
public double BottomRightLon { get; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public SatTile(int x, int y, int zoom, string url)
|
||||||
|
{
|
||||||
|
X = x;
|
||||||
|
Y = y;
|
||||||
|
Url = url;
|
||||||
|
|
||||||
|
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
|
||||||
|
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
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 void InitializeDirs(DirectoriesConfig config)
|
||||||
|
{
|
||||||
|
_labelsDir = config.LabelsDirectory;
|
||||||
|
_imagesDir = config.ImagesDirectory;
|
||||||
|
_thumbDir = config.ThumbnailsDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[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");
|
||||||
|
|
||||||
|
#endregion Calculated
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AnnotationImage : Annotation
|
||||||
|
{
|
||||||
|
[Key("i")] public byte[] Image { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AnnotationStatus
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Created = 10,
|
||||||
|
Validated = 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class AnnotationName
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ 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<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
|
||||||
public ITable<Detection> Detections => this.GetTable<Detection>();
|
public ITable<Detection> Detections => this.GetTable<Detection>();
|
||||||
|
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using System.IO;
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
|
using LinqToDB.Data;
|
||||||
using LinqToDB.DataProvider.SQLite;
|
using LinqToDB.DataProvider.SQLite;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -16,6 +17,8 @@ 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 Run(Func<AnnotationsDb, Task> func);
|
||||||
void SaveToDisk();
|
void SaveToDisk();
|
||||||
|
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DbFactory : IDbFactory
|
public class DbFactory : IDbFactory
|
||||||
@@ -37,10 +40,10 @@ public class DbFactory : IDbFactory
|
|||||||
_memoryConnection = new SQLiteConnection(MemoryConnStr);
|
_memoryConnection = new SQLiteConnection(MemoryConnStr);
|
||||||
_memoryConnection.Open();
|
_memoryConnection.Open();
|
||||||
_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,7 +51,6 @@ 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();
|
CreateDb();
|
||||||
@@ -63,6 +65,20 @@ public class DbFactory : IDbFactory
|
|||||||
db.CreateTable<Annotation>();
|
db.CreateTable<Annotation>();
|
||||||
db.CreateTable<AnnotationName>();
|
db.CreateTable<AnnotationName>();
|
||||||
db.CreateTable<Detection>();
|
db.CreateTable<Detection>();
|
||||||
|
db.CreateTable<QueueOffset>();
|
||||||
|
db.QueueOffsets.BulkCopy(new List<QueueOffset>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Offset = 0,
|
||||||
|
QueueName = Constants.MQ_ANNOTATIONS_QUEUE
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Offset = 0,
|
||||||
|
QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||||
@@ -81,6 +97,23 @@ public class DbFactory : IDbFactory
|
|||||||
{
|
{
|
||||||
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var names = annotations.Select(x => x.Name).ToList();
|
||||||
|
await DeleteAnnotations(names, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await Run(async db =>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations");
|
||||||
|
});
|
||||||
|
SaveToDisk();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AnnotationsDbSchemaHolder
|
public static class AnnotationsDbSchemaHolder
|
||||||
@@ -92,10 +125,19 @@ public static class AnnotationsDbSchemaHolder
|
|||||||
MappingSchema = new MappingSchema();
|
MappingSchema = new MappingSchema();
|
||||||
var builder = new FluentMappingBuilder(MappingSchema);
|
var builder = new FluentMappingBuilder(MappingSchema);
|
||||||
|
|
||||||
builder.Entity<Annotation>()
|
var annotationBuilder = builder.Entity<Annotation>();
|
||||||
.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
|
||||||
.HasPrimaryKey(x => x.Name)
|
.HasPrimaryKey(x => x.Name)
|
||||||
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName);
|
.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>()
|
builder.Entity<Detection>()
|
||||||
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
.HasTableName(Constants.DETECTIONS_TABLENAME);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.Common.Database;
|
||||||
|
|
||||||
|
public class QueueOffset
|
||||||
|
{
|
||||||
|
public string QueueName { get; set; } = null!;
|
||||||
|
public ulong Offset { get; set; }
|
||||||
|
}
|
||||||
+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,9 @@
|
|||||||
|
using Azaion.Common.Database;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Events;
|
||||||
|
|
||||||
|
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
|
||||||
|
{
|
||||||
|
public List<Annotation> Annotations { get; set; } = annotations;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -7,8 +7,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;
|
||||||
|
|||||||
@@ -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,39 @@
|
|||||||
|
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 lat, double lon) 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 (latDeg, lonDeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
|
||||||
|
{
|
||||||
|
var latRad = centerLat * Math.PI / 180.0;
|
||||||
|
|
||||||
|
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
|
||||||
|
var minLat = Math.Max(centerLat - latDiff, -90.0);
|
||||||
|
var maxLat = Math.Min(centerLat + latDiff, 90.0);
|
||||||
|
|
||||||
|
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
|
||||||
|
var minLon = Math.Max(centerLon - lonDiff, -180.0);
|
||||||
|
var maxLon = Math.Min(centerLon + 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,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,72 @@
|
|||||||
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)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(action);
|
||||||
|
if (actionId == Guid.Empty)
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
|
if (!state.IsCoolingDown)
|
||||||
_throttleOn = false;
|
{
|
||||||
}, cancellationToken);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,12 @@ using Azaion.Common.Database;
|
|||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.Common.Events;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.CommonSecurity.DTO;
|
||||||
using Azaion.CommonSecurity.Services;
|
using Azaion.CommonSecurity.Services;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
|
using LinqToDB.Data;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -17,115 +20,147 @@ using RabbitMQ.Stream.Client.Reliable;
|
|||||||
|
|
||||||
namespace Azaion.Common.Services;
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
public class AnnotationService
|
public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
||||||
{
|
{
|
||||||
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 QueueConfig _queueConfig;
|
private readonly QueueConfig _queueConfig;
|
||||||
private Consumer _consumer = null!;
|
private Consumer _consumer = null!;
|
||||||
|
private readonly UIConfig _uiConfig;
|
||||||
|
private static readonly Guid SaveTaskId = 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)
|
||||||
{
|
{
|
||||||
_apiClient = apiClient;
|
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_producer = producer;
|
_producer = producer;
|
||||||
_galleryService = galleryService;
|
_galleryService = galleryService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
_api = api;
|
||||||
_queueConfig = queueConfig.Value;
|
_queueConfig = queueConfig.Value;
|
||||||
|
_uiConfig = uiConfig.Value;
|
||||||
|
|
||||||
Task.Run(async () => await Init()).Wait();
|
Task.Run(async () => await Init()).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Init()
|
private async Task Init(CancellationToken cancellationToken = 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 + 1),
|
||||||
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)),
|
MessageHandler = async (_, _, context, message) =>
|
||||||
|
{
|
||||||
|
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
|
||||||
|
|
||||||
|
offsets.AnnotationsOffset = context.Offset;
|
||||||
|
ThrottleExt.Throttle(() =>
|
||||||
|
{
|
||||||
|
_api.UpdateOffsets(offsets);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
|
||||||
|
|
||||||
|
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
|
||||||
|
return;
|
||||||
|
|
||||||
|
await SaveAnnotationInner(
|
||||||
|
msg.CreatedDate,
|
||||||
|
msg.OriginalMediaName,
|
||||||
|
msg.Time,
|
||||||
|
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
|
||||||
|
msg.Source,
|
||||||
|
new MemoryStream(msg.Image),
|
||||||
|
msg.CreatedRole,
|
||||||
|
msg.CreatedEmail,
|
||||||
|
fromQueue: true,
|
||||||
|
token: cancellationToken);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//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.Now, a.OriginalMediaName, 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, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
|
||||||
|
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream,
|
||||||
|
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
|
||||||
|
|
||||||
|
// Manual save from Validators -> Validated -> stream: azaion-annotations-confirm
|
||||||
|
// AI, Manual save from Operators -> Created -> stream: azaion-annotations
|
||||||
|
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List<Detection> detections, SourceEnum source, Stream? stream,
|
||||||
|
RoleEnum userRole,
|
||||||
string createdEmail,
|
string createdEmail,
|
||||||
|
bool fromQueue = false,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
//Flow for roles:
|
|
||||||
// Operator:
|
|
||||||
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
|
|
||||||
// Validator:
|
|
||||||
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
|
|
||||||
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
|
|
||||||
|
|
||||||
var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
|
||||||
AnnotationStatus status;
|
AnnotationStatus status;
|
||||||
|
var fName = originalMediaName.ToTimeName(time);
|
||||||
var annotation = await _dbFactory.Run(async db =>
|
var annotation = await _dbFactory.Run(async db =>
|
||||||
{
|
{
|
||||||
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
||||||
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator
|
status = userRole.IsValidator() && source == SourceEnum.Manual
|
||||||
? AnnotationStatus.Validated
|
? AnnotationStatus.Validated
|
||||||
: AnnotationStatus.Created;
|
: AnnotationStatus.Created;
|
||||||
|
|
||||||
|
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
|
||||||
|
|
||||||
if (ann != null)
|
if (ann != null)
|
||||||
|
{
|
||||||
await db.Annotations
|
await db.Annotations
|
||||||
.Where(x => x.Name == fName)
|
.Where(x => x.Name == fName)
|
||||||
.Set(x => x.Classes, classes)
|
|
||||||
.Set(x => x.Source, source)
|
.Set(x => x.Source, source)
|
||||||
.Set(x => x.AnnotationStatus, status)
|
.Set(x => x.AnnotationStatus, status)
|
||||||
|
.Set(x => x.CreatedDate, createdDate)
|
||||||
|
.Set(x => x.CreatedEmail, createdEmail)
|
||||||
|
.Set(x => x.CreatedRole, userRole)
|
||||||
.UpdateAsync(token: token);
|
.UpdateAsync(token: token);
|
||||||
|
ann.Detections = detections;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ann = new Annotation
|
ann = new Annotation
|
||||||
{
|
{
|
||||||
CreatedDate = createdDate,
|
CreatedDate = createdDate,
|
||||||
Name = fName,
|
Name = fName,
|
||||||
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,9 +170,53 @@ public class AnnotationService
|
|||||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
||||||
}
|
}
|
||||||
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
|
||||||
|
|
||||||
await _galleryService.CreateThumbnail(annotation, token);
|
await _galleryService.CreateThumbnail(annotation, token);
|
||||||
await _producer.SendToQueue(annotation, token);
|
if (_uiConfig.GenerateAnnotatedImage)
|
||||||
|
await _galleryService.CreateAnnotatedImage(annotation, token);
|
||||||
|
|
||||||
|
if (!fromQueue && !_uiConfig.SilentDetection) //Send to queue only if we're not getting from queue already
|
||||||
|
await _producer.SendToInnerQueue(annotation, token);
|
||||||
|
|
||||||
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
|
||||||
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
_dbFactory.SaveToDisk();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
|
return annotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateAnnotations(List<Annotation> annotations, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (!_api.CurrentUser.Role.IsValidator())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var annNames = annotations.Select(x => x.Name).ToHashSet();
|
||||||
|
await _dbFactory.Run(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);
|
||||||
|
});
|
||||||
|
ThrottleExt.Throttle(async () =>
|
||||||
|
{
|
||||||
|
_dbFactory.SaveToDisk();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken);
|
||||||
|
foreach (var annotation in notification.Annotations)
|
||||||
|
{
|
||||||
|
File.Delete(annotation.ImagePath);
|
||||||
|
File.Delete(annotation.LabelPath);
|
||||||
|
File.Delete(annotation.ThumbPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
@@ -53,7 +52,7 @@ public class FailsafeAnnotationsProducer
|
|||||||
await Init(cancellationToken);
|
await Init(cancellationToken);
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
var messages = await GetFromQueue(cancellationToken);
|
var messages = await GetFromInnerQueue(cancellationToken);
|
||||||
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
||||||
{
|
{
|
||||||
var sent = false;
|
var sent = false;
|
||||||
@@ -65,58 +64,82 @@ public class FailsafeAnnotationsProducer
|
|||||||
.Where(x => x.Status == AnnotationStatus.Created)
|
.Where(x => x.Status == AnnotationStatus.Created)
|
||||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||||
.ToList();
|
.ToList();
|
||||||
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
if (createdMessages.Any())
|
||||||
|
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
||||||
|
|
||||||
var validatedMessages = messagesChunk
|
var validatedMessages = messagesChunk
|
||||||
.Where(x => x.Status == AnnotationStatus.Validated)
|
.Where(x => x.Status == AnnotationStatus.Validated)
|
||||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||||
.ToList();
|
.ToList();
|
||||||
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
|
if (validatedMessages.Any())
|
||||||
|
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
|
||||||
|
|
||||||
await _dbFactory.Run(async db =>
|
await _dbFactory.Run(async db =>
|
||||||
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
|
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
|
||||||
sent = true;
|
sent = true;
|
||||||
|
_dbFactory.SaveToDisk();
|
||||||
}
|
}
|
||||||
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), cancellationToken);
|
||||||
}
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default)
|
private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _dbFactory.Run(async db =>
|
return await _dbFactory.Run(async db =>
|
||||||
{
|
{
|
||||||
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a)
|
var annotations = await db.AnnotationsQueue.Join(
|
||||||
|
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a)
|
||||||
.ToListAsync(token: cancellationToken);
|
.ToListAsync(token: cancellationToken);
|
||||||
|
|
||||||
var messages = new List<AnnotationCreatedMessage>();
|
var messages = new List<AnnotationCreatedMessage>();
|
||||||
|
var badImages = new List<string>();
|
||||||
foreach (var annotation in annotations)
|
foreach (var annotation in annotations)
|
||||||
{
|
{
|
||||||
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
try
|
||||||
var annCreateMessage = new AnnotationCreatedMessage
|
|
||||||
{
|
{
|
||||||
Name = annotation.Name,
|
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
||||||
|
var annCreateMessage = new AnnotationCreatedMessage
|
||||||
|
{
|
||||||
|
Name = annotation.Name,
|
||||||
|
OriginalMediaName = annotation.OriginalMediaName,
|
||||||
|
Time = annotation.Time,
|
||||||
|
CreatedRole = annotation.CreatedRole,
|
||||||
|
CreatedEmail = annotation.CreatedEmail,
|
||||||
|
CreatedDate = annotation.CreatedDate,
|
||||||
|
Status = annotation.AnnotationStatus,
|
||||||
|
|
||||||
CreatedRole = annotation.CreatedRole,
|
ImageExtension = annotation.ImageExtension,
|
||||||
CreatedEmail = annotation.CreatedEmail,
|
Image = image,
|
||||||
CreatedDate = annotation.CreatedDate,
|
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
||||||
|
Source = annotation.Source,
|
||||||
|
};
|
||||||
|
messages.Add(annCreateMessage);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
badImages.Add(annotation.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image = image,
|
if (badImages.Any())
|
||||||
Detections = JsonConvert.SerializeObject(annotation.Detections),
|
{
|
||||||
Source = annotation.Source
|
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken);
|
||||||
};
|
_dbFactory.SaveToDisk();
|
||||||
messages.Add(annCreateMessage);
|
|
||||||
}
|
}
|
||||||
return messages;
|
return messages;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _dbFactory.Run(async db =>
|
await _dbFactory.Run(async db =>
|
||||||
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IGpsMatcherService
|
||||||
|
{
|
||||||
|
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
|
||||||
|
void StopGpsMatching();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
|
||||||
|
{
|
||||||
|
private const int ZOOM_LEVEL = 18;
|
||||||
|
private const int POINTS_COUNT = 5;
|
||||||
|
private const int DISTANCE_BETWEEN_POINTS_M = 100;
|
||||||
|
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
|
||||||
|
|
||||||
|
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
|
||||||
|
{
|
||||||
|
var currentLat = initialLatitude;
|
||||||
|
var currentLon = initialLongitude;
|
||||||
|
|
||||||
|
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
|
||||||
|
if (Directory.Exists(routeDir))
|
||||||
|
Directory.Delete(routeDir, true);
|
||||||
|
Directory.CreateDirectory(routeDir);
|
||||||
|
|
||||||
|
var routeFiles = new List<string>();
|
||||||
|
foreach (var file in Directory.GetFiles(userRouteDir))
|
||||||
|
{
|
||||||
|
routeFiles.Add(file);
|
||||||
|
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexOffset = 0;
|
||||||
|
while (routeFiles.Any())
|
||||||
|
{
|
||||||
|
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
|
||||||
|
gpsMatcherClient.StartMatching(new StartMatchingEvent
|
||||||
|
{
|
||||||
|
ImagesCount = POINTS_COUNT,
|
||||||
|
Latitude = initialLatitude,
|
||||||
|
Longitude = initialLongitude,
|
||||||
|
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
|
||||||
|
RouteDir = dirConfig.Value.GpsRouteDirectory
|
||||||
|
});
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = gpsMatcherClient.GetResult();
|
||||||
|
if (result == null)
|
||||||
|
break;
|
||||||
|
result.Index += indexOffset;
|
||||||
|
await processResult(result);
|
||||||
|
currentLat = result.Latitude;
|
||||||
|
currentLon = result.Longitude;
|
||||||
|
routeFiles.RemoveAt(0);
|
||||||
|
}
|
||||||
|
indexOffset += POINTS_COUNT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopGpsMatching()
|
||||||
|
{
|
||||||
|
gpsMatcherClient.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,6 +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.Common.Extensions;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Azaion.CommonSecurity.DTO;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Data;
|
using LinqToDB.Data;
|
||||||
@@ -72,10 +73,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 +89,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 +97,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 +106,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,8 +146,15 @@ public class GalleryService(
|
|||||||
AnnotationStatus = AnnotationStatus.Validated
|
AnnotationStatus = AnnotationStatus.Validated
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Remove duplicates
|
||||||
if (!existingAnnotations.ContainsKey(fName))
|
if (!existingAnnotations.ContainsKey(fName))
|
||||||
missedAnnotations.Add(annotation);
|
{
|
||||||
|
if (missedAnnotations.ContainsKey(fName))
|
||||||
|
Console.WriteLine($"{fName} is already exists! Duplicate!");
|
||||||
|
else
|
||||||
|
missedAnnotations.TryAdd(fName, annotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!thumbnails.Contains(fName))
|
if (!thumbnails.Contains(fName))
|
||||||
await CreateThumbnail(annotation, cancellationToken);
|
await CreateThumbnail(annotation, cancellationToken);
|
||||||
@@ -129,18 +165,22 @@ public class GalleryService(
|
|||||||
logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
|
logger.LogError(e, $"Failed to generate thumbnail for {file.Name}! Error: {e.Message}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new ParallelOptions
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
ProgressFn = async num =>
|
||||||
{
|
{
|
||||||
ProgressFn = async num =>
|
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
|
||||||
{
|
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
||||||
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
|
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
|
||||||
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
|
await Task.CompletedTask;
|
||||||
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
|
},
|
||||||
await Task.CompletedTask;
|
CpuUtilPercent = 100,
|
||||||
},
|
ProgressUpdateInterval = 200
|
||||||
CpuUtilPercent = 100,
|
});
|
||||||
ProgressUpdateInterval = 200
|
}
|
||||||
});
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, $"Failed to refresh thumbnails! Error: {e.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -148,15 +188,20 @@ public class GalleryService(
|
|||||||
{
|
{
|
||||||
MaxBatchSize = 50
|
MaxBatchSize = 50
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Db could be updated during the long files scraping
|
||||||
|
existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
|
||||||
|
await db.Annotations.ToDictionaryAsync(x => x.Name)));
|
||||||
|
var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList();
|
||||||
|
var annotationsToInsert = missedAnnotations
|
||||||
|
.Where(a => !existingAnnotations.ContainsKey(a.Key))
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
await dbFactory.Run(async db =>
|
await dbFactory.Run(async db =>
|
||||||
{
|
{
|
||||||
var xx = missedAnnotations.GroupBy(x => x.Name)
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
|
||||||
.Where(gr => gr.Count() > 1)
|
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
|
||||||
.ToList();
|
|
||||||
foreach (var gr in xx)
|
|
||||||
Console.WriteLine(gr.Key);
|
|
||||||
await db.BulkCopyAsync(copyOptions, missedAnnotations);
|
|
||||||
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections));
|
|
||||||
});
|
});
|
||||||
dbFactory.SaveToDisk();
|
dbFactory.SaveToDisk();
|
||||||
_updateLock.Release();
|
_updateLock.Release();
|
||||||
@@ -227,8 +272,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.X - frameX) / scale), (float)((label.Y - 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,6 +282,25 @@ public class GalleryService(
|
|||||||
logger.LogError(e, e.Message);
|
logger.LogError(e, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token)
|
||||||
|
{
|
||||||
|
var 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.X, (float)det.Y, (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.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black);
|
||||||
|
}
|
||||||
|
originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IGalleryService
|
public interface IGalleryService
|
||||||
@@ -248,4 +311,5 @@ public interface IGalleryService
|
|||||||
Task RefreshThumbnails();
|
Task RefreshThumbnails();
|
||||||
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
Task ClearThumbnails(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task CreateAnnotatedImage(Annotation annotation, CancellationToken token);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NetMQ;
|
||||||
|
using NetMQ.Sockets;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IGpsMatcherClient
|
||||||
|
{
|
||||||
|
|
||||||
|
void StartMatching(StartMatchingEvent startEvent);
|
||||||
|
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StartMatchingEvent
|
||||||
|
{
|
||||||
|
public string RouteDir { get; set; } = null!;
|
||||||
|
public string SatelliteImagesDir { get; set; } = null!;
|
||||||
|
public int ImagesCount { get; set; }
|
||||||
|
public double Latitude { get; set; }
|
||||||
|
public double Longitude { get; set; }
|
||||||
|
public string ProcessingType { get; set; } = "cuda";
|
||||||
|
public int Altitude { get; set; } = 400;
|
||||||
|
public double CameraSensorWidth { get; set; } = 23.5;
|
||||||
|
public double CameraFocalLength { get; set; } = 24;
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsMatcherClient : IGpsMatcherClient
|
||||||
|
{
|
||||||
|
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
|
||||||
|
private readonly RequestSocket _requestSocket = new();
|
||||||
|
private readonly SubscriberSocket _subscriberSocket = new();
|
||||||
|
|
||||||
|
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
|
||||||
|
{
|
||||||
|
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
|
||||||
|
Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = SecurityConstants.ExternalGpsDeniedPath,
|
||||||
|
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
|
||||||
|
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
|
||||||
|
//RedirectStandardOutput = true,
|
||||||
|
//RedirectStandardError = true,
|
||||||
|
//CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||||
|
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||||
|
//process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
//throw;
|
||||||
|
}
|
||||||
|
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
|
||||||
|
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
|
||||||
|
_subscriberSocket.Subscribe("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartMatching(StartMatchingEvent e)
|
||||||
|
{
|
||||||
|
_requestSocket.SendFrame(e.ToString());
|
||||||
|
var response = _requestSocket.ReceiveFrameString();
|
||||||
|
if (response != "OK")
|
||||||
|
throw new Exception("Start Matching Failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var tryNum = 0;
|
||||||
|
while (!ct.IsCancellationRequested && tryNum++ < retries)
|
||||||
|
{
|
||||||
|
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
|
||||||
|
continue;
|
||||||
|
if (update == "FINISHED")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var parts = update.Split(',');
|
||||||
|
if (parts.Length != 5)
|
||||||
|
throw new Exception("Matching Result Failed");
|
||||||
|
|
||||||
|
return new GpsMatchResult
|
||||||
|
{
|
||||||
|
Index = int.Parse(parts[0]),
|
||||||
|
Image = parts[1],
|
||||||
|
Latitude = double.Parse(parts[2]),
|
||||||
|
Longitude = double.Parse(parts[3]),
|
||||||
|
MatchType = parts[4]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ct.IsCancellationRequested)
|
||||||
|
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_requestSocket.SendFrame("STOP");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Azaion.Common.Database;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO.Commands;
|
||||||
|
using Azaion.CommonSecurity.Services;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface IInferenceService
|
||||||
|
{
|
||||||
|
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default);
|
||||||
|
void StopInference();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IAzaionApi azaionApi, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
|
||||||
|
{
|
||||||
|
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
|
||||||
|
{
|
||||||
|
client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials));
|
||||||
|
var aiConfig = aiConfigOptions.Value;
|
||||||
|
|
||||||
|
aiConfig.Paths = mediaPaths;
|
||||||
|
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
|
||||||
|
|
||||||
|
while (!detectToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = client.GetBytes(ct: detectToken);
|
||||||
|
if (bytes == null)
|
||||||
|
throw new Exception("Can't get bytes from inference client");
|
||||||
|
|
||||||
|
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
|
||||||
|
return;
|
||||||
|
|
||||||
|
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken);
|
||||||
|
|
||||||
|
await processAnnotation(annotationImage);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, e.Message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInference()
|
||||||
|
{
|
||||||
|
client.Send(RemoteCommand.Create(CommandType.StopInference));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Azaion.Common.DTO;
|
||||||
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Extensions;
|
||||||
|
using Azaion.CommonSecurity;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
|
namespace Azaion.Common.Services;
|
||||||
|
|
||||||
|
public interface ISatelliteDownloader
|
||||||
|
{
|
||||||
|
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SatelliteDownloader(
|
||||||
|
ILogger<SatelliteDownloader> logger,
|
||||||
|
IOptions<MapConfig> mapConfig,
|
||||||
|
IOptions<DirectoriesConfig> directoriesConfig,
|
||||||
|
IHttpClientFactory httpClientFactory)
|
||||||
|
: ISatelliteDownloader
|
||||||
|
{
|
||||||
|
private const int INPUT_TILE_SIZE = 256;
|
||||||
|
|
||||||
|
private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}";
|
||||||
|
private const int NUM_SERVERS = 4;
|
||||||
|
|
||||||
|
private const int CROP_WIDTH = 1024;
|
||||||
|
private const int CROP_HEIGHT = 1024;
|
||||||
|
private const int STEP_X = 300;
|
||||||
|
private const int STEP_Y = 300;
|
||||||
|
private const int OUTPUT_TILE_SIZE = 512;
|
||||||
|
|
||||||
|
private readonly string _apiKey = mapConfig.Value.ApiKey;
|
||||||
|
private readonly string _satDirectory = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
|
||||||
|
|
||||||
|
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
//empty Satellite directory
|
||||||
|
if (Directory.Exists(_satDirectory))
|
||||||
|
Directory.Delete(_satDirectory, true);
|
||||||
|
Directory.CreateDirectory(_satDirectory);
|
||||||
|
|
||||||
|
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
|
||||||
|
var image = ComposeTiles(downloadTilesResult.Tiles, token);
|
||||||
|
if (image != null)
|
||||||
|
await SplitToTiles(image, downloadTilesResult, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
// Calculate all crop parameters beforehand
|
||||||
|
var cropTasks = new List<Action>();
|
||||||
|
var latRange = bounds.LatMax - bounds.LatMin; // [cite: 13]
|
||||||
|
var lonRange = bounds.LonMax - bounds.LonMin; // [cite: 13]
|
||||||
|
var degreesPerPixelLat = latRange / image.Height; // [cite: 13]
|
||||||
|
var degreesPerPixelLon = lonRange / image.Width; // [cite: 14]
|
||||||
|
int tempRowIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
|
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y) // [cite: 15]
|
||||||
|
{
|
||||||
|
int tempColIndex = 0;
|
||||||
|
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X) // [cite: 16]
|
||||||
|
{
|
||||||
|
// Capture loop variables for the closure
|
||||||
|
int currentTop = top;
|
||||||
|
int currentLeft = left;
|
||||||
|
int rowIndex = tempRowIndex;
|
||||||
|
int colIndex = tempColIndex;
|
||||||
|
|
||||||
|
cropTasks.Add(() =>
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var cropBox = new Rectangle(currentLeft, currentTop, CROP_WIDTH, CROP_HEIGHT);
|
||||||
|
|
||||||
|
using var croppedImage = image.Clone(ctx => ctx.Crop(cropBox));
|
||||||
|
var cropTlLat = bounds.LatMax - (currentTop * degreesPerPixelLat);
|
||||||
|
var cropTlLon = bounds.LonMin + (currentLeft * degreesPerPixelLon);
|
||||||
|
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
|
||||||
|
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
|
||||||
|
|
||||||
|
var outputFilename = Path.Combine(_satDirectory,
|
||||||
|
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
|
||||||
|
);
|
||||||
|
|
||||||
|
using var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3));
|
||||||
|
resizedImage.SaveAsTiffAsync(outputFilename, token).GetAwaiter().GetResult(); // Use synchronous saving or manage async Tasks properly in parallel context
|
||||||
|
});
|
||||||
|
tempColIndex++;
|
||||||
|
}
|
||||||
|
tempRowIndex++;
|
||||||
|
}
|
||||||
|
// Execute tasks in parallel
|
||||||
|
await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Image<Rgba32>? ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (downloadedTiles.IsEmpty)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var xMin = downloadedTiles.Min(t => t.Key.x);
|
||||||
|
var xMax = downloadedTiles.Max(t => t.Key.x);
|
||||||
|
var yMin = downloadedTiles.Min(t => t.Key.y);
|
||||||
|
var yMax = downloadedTiles.Max(t => t.Key.y);
|
||||||
|
|
||||||
|
var totalWidth = (xMax - xMin + 1) * INPUT_TILE_SIZE;
|
||||||
|
var totalHeight = (yMax - yMin + 1) * INPUT_TILE_SIZE;
|
||||||
|
|
||||||
|
if (totalWidth <= 0 || totalHeight <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var largeImage = new Image<Rgba32>(totalWidth, totalHeight);
|
||||||
|
|
||||||
|
largeImage.Mutate(ctx =>
|
||||||
|
{
|
||||||
|
for (var y = yMin; y <= yMax; y++)
|
||||||
|
{
|
||||||
|
for (var x = xMin; x <= xMax; x++)
|
||||||
|
{
|
||||||
|
if (!downloadedTiles.TryGetValue((x, y), out var tileData))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var tileImage = Image.Load(tileData);
|
||||||
|
var offsetX = (x - xMin) * INPUT_TILE_SIZE;
|
||||||
|
var offsetY = (y - yMin) * INPUT_TILE_SIZE;
|
||||||
|
ctx.DrawImage(tileImage, new Point(offsetX, offsetY), 1f);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error while loading tile: {tileData}");
|
||||||
|
}
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return largeImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SessionResponse(string Session);
|
||||||
|
|
||||||
|
private async Task<string?> GetSessionToken()
|
||||||
|
{
|
||||||
|
var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}";
|
||||||
|
using var httpClient = httpClientFactory.CreateClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
|
||||||
|
var response = await httpClient.PostAsync(url, new StringContent(str));
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>();
|
||||||
|
return sessionResponse?.Session;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, e.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM);
|
||||||
|
|
||||||
|
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
|
||||||
|
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
|
||||||
|
|
||||||
|
var tilesToDownload = new ConcurrentQueue<SatTile>();
|
||||||
|
var downloadedTiles = new ConcurrentDictionary<(int x, int y), byte[]>();
|
||||||
|
var server = 0;
|
||||||
|
var sessionToken = await GetSessionToken();
|
||||||
|
|
||||||
|
for (var y = yMin; y <= yMax + 1; y++)
|
||||||
|
for (var x = xMin; x <= xMax + 1; x++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
var url = string.Format(TILE_URL_TEMPLATE, server, x, y, zoomLevel, sessionToken);
|
||||||
|
|
||||||
|
tilesToDownload.Enqueue(new SatTile(x, y, zoomLevel, url));
|
||||||
|
server = (server + 1) % NUM_SERVERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadTasks = new List<Task>();
|
||||||
|
int downloadedCount = 0;
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_SERVERS; i++)
|
||||||
|
{
|
||||||
|
downloadTasks.Add(Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var httpClient = httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
while (tilesToDownload.TryDequeue(out var tileInfo))
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested) break;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36");
|
||||||
|
var response = await httpClient.GetAsync(tileInfo.Url, token);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var tileData = await response.Content.ReadAsByteArrayAsync(token);
|
||||||
|
if (tileData?.Length > 0)
|
||||||
|
{
|
||||||
|
downloadedTiles.TryAdd((tileInfo.X, tileInfo.Y), tileData);
|
||||||
|
Interlocked.Increment(ref downloadedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException requestException)
|
||||||
|
{
|
||||||
|
logger.LogError(requestException, $"Fail to download tile! Url: {tileInfo.Url}. {requestException.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, $"Fail to download tile! {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(downloadTasks);
|
||||||
|
return new DownloadTilesResult
|
||||||
|
{
|
||||||
|
Tiles = downloadedTiles,
|
||||||
|
LatMin = latMin,
|
||||||
|
LatMax = latMax,
|
||||||
|
LonMin = lonMin,
|
||||||
|
LonMax = lonMax
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
|
||||||
|
<PackageReference Include="MessagePack" Version="3.1.0" />
|
||||||
|
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||||
|
<PackageReference Include="NetMQ" Version="4.0.1.13" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
|
||||||
|
|
||||||
public class ApiConfig
|
|
||||||
{
|
|
||||||
public string Url { get; set; } = null!;
|
|
||||||
public int RetryCount {get;set;}
|
|
||||||
public double TimeoutSeconds { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
public class ApiCredentials(string email, string password) : EventArgs
|
public class ApiCredentials(string email, string password) : EventArgs
|
||||||
{
|
{
|
||||||
|
[Key(nameof(Email))]
|
||||||
public string Email { get; set; } = email;
|
public string Email { get; set; } = email;
|
||||||
|
|
||||||
|
[Key(nameof(Password))]
|
||||||
public string Password { get; set; } = password;
|
public string Password { get; set; } = password;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
internal class BusinessExceptionDto
|
||||||
|
{
|
||||||
|
public int ErrorCode { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.DTO.Commands;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class RemoteCommand(CommandType commandType, byte[]? data = null)
|
||||||
|
{
|
||||||
|
[Key("CommandType")]
|
||||||
|
public CommandType CommandType { get; set; } = commandType;
|
||||||
|
|
||||||
|
[Key("Data")]
|
||||||
|
public byte[]? Data { get; set; } = data;
|
||||||
|
|
||||||
|
public static RemoteCommand Create(CommandType commandType) =>
|
||||||
|
new(commandType);
|
||||||
|
|
||||||
|
public static RemoteCommand Create<T>(CommandType commandType, T data) where T : class =>
|
||||||
|
new(commandType, MessagePackSerializer.Serialize(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[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,
|
||||||
|
Login = 10,
|
||||||
|
Load = 20,
|
||||||
|
Inference = 30,
|
||||||
|
StopInference = 40,
|
||||||
|
Exit = 100
|
||||||
|
}
|
||||||
+7
-1
@@ -1,10 +1,16 @@
|
|||||||
namespace Azaion.Common.DTO.Config;
|
namespace Azaion.CommonSecurity.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,19 @@
|
|||||||
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
public abstract class ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ZeroMqHost { get; set; } = "";
|
||||||
|
public int ZeroMqPort { get; set; }
|
||||||
|
public double OneTryTimeoutSeconds { get; set; }
|
||||||
|
public int RetryCount {get;set;}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public string ApiUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GpsDeniedClientConfig : ExternalClientConfig
|
||||||
|
{
|
||||||
|
public int ZeroMqSubscriberPort { get; set; }
|
||||||
|
}
|
||||||
@@ -6,6 +6,4 @@ public class HardwareInfo
|
|||||||
public string GPU { get; set; } = null!;
|
public string GPU { get; set; } = null!;
|
||||||
public string MacAddress { get; set; } = null!;
|
public string MacAddress { get; set; } = null!;
|
||||||
public string Memory { get; set; } = null!;
|
public string Memory { get; set; } = null!;
|
||||||
|
|
||||||
public string Hash { get; set; } = null!;
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Azaion.CommonSecurity.DTO;
|
using Azaion.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public enum RoleEnum
|
public enum RoleEnum
|
||||||
{
|
{
|
||||||
@@ -7,6 +9,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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
|
|
||||||
public class SecureAppConfig
|
public class SecureAppConfig
|
||||||
{
|
{
|
||||||
public ApiConfig ApiConfig { get; set; } = null!;
|
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
|
||||||
|
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
|
||||||
|
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.DTO;
|
namespace Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public string Id { get; set; } = "";
|
||||||
public string Email { get; set; }
|
public string Email { get; set; } = "";
|
||||||
public RoleEnum Role { get; set; }
|
public RoleEnum Role { get; set; }
|
||||||
|
public UserConfig? UserConfig { get; set; } = null!;
|
||||||
public User(IEnumerable<Claim> claims)
|
}
|
||||||
{
|
|
||||||
var claimDict = claims.ToDictionary(x => x.Type, x => x.Value);
|
public class UserConfig
|
||||||
|
{
|
||||||
Id = Guid.Parse(claimDict[SecurityConstants.CLAIM_NAME_ID]);
|
public UserQueueOffsets? QueueOffsets { get; set; } = new();
|
||||||
Email = claimDict[SecurityConstants.CLAIM_EMAIL];
|
}
|
||||||
if (!Enum.TryParse(claimDict[SecurityConstants.CLAIM_ROLE], out RoleEnum role))
|
|
||||||
role = RoleEnum.None;
|
public class UserQueueOffsets
|
||||||
Role = role;
|
{
|
||||||
}
|
public ulong AnnotationsOffset { get; set; }
|
||||||
|
public ulong AnnotationsConfirmOffset { get; set; }
|
||||||
|
public ulong AnnotationsCommandsOffset { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,55 @@
|
|||||||
namespace Azaion.CommonSecurity;
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity;
|
||||||
|
|
||||||
public class SecurityConstants
|
public class SecurityConstants
|
||||||
{
|
{
|
||||||
public const string CONFIG_PATH = "config.json";
|
public const string CONFIG_PATH = "config.json";
|
||||||
|
|
||||||
#region ApiConfig
|
public const string DUMMY_DIR = "dummy";
|
||||||
|
|
||||||
public const string DEFAULT_API_URL = "https://api.azaion.com/";
|
#region ExternalClientsConfig
|
||||||
public const int DEFAULT_API_RETRY_COUNT = 3;
|
|
||||||
public const int DEFAULT_API_TIMEOUT_SECONDS = 40;
|
|
||||||
|
|
||||||
public const string CLAIM_NAME_ID = "nameid";
|
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
|
||||||
public const string CLAIM_EMAIL = "unique_name";
|
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
|
||||||
public const string CLAIM_ROLE = "role";
|
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
|
||||||
|
|
||||||
#endregion ApiConfig
|
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
|
||||||
|
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
|
||||||
|
|
||||||
|
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
|
||||||
|
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
|
||||||
|
|
||||||
|
public const int DEFAULT_RETRY_COUNT = 25;
|
||||||
|
public const int DEFAULT_TIMEOUT_SECONDS = 5;
|
||||||
|
|
||||||
|
# region Cache keys
|
||||||
|
|
||||||
|
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
|
||||||
|
public const string HARDWARE_INFO_KEY = "HardwareInfo";
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
public static readonly SecureAppConfig DefaultSecureAppConfig = new()
|
||||||
|
{
|
||||||
|
InferenceClientConfig = new InferenceClientConfig
|
||||||
|
{
|
||||||
|
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
|
||||||
|
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
|
||||||
|
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
||||||
|
RetryCount = DEFAULT_RETRY_COUNT
|
||||||
|
},
|
||||||
|
GpsDeniedClientConfig = new GpsDeniedClientConfig
|
||||||
|
{
|
||||||
|
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
|
||||||
|
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
|
||||||
|
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
|
||||||
|
RetryCount = DEFAULT_RETRY_COUNT,
|
||||||
|
},
|
||||||
|
DirectoriesConfig = new DirectoriesConfig
|
||||||
|
{
|
||||||
|
ApiResourcesDirectory = ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endregion ExternalClientsConfig
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
|
public interface IAzaionApi
|
||||||
|
{
|
||||||
|
ApiCredentials Credentials { get; }
|
||||||
|
User CurrentUser { get; }
|
||||||
|
void UpdateOffsets(UserQueueOffsets offsets);
|
||||||
|
//Stream GetResource(string filename, string folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
|
||||||
|
{
|
||||||
|
private string _jwtToken = null!;
|
||||||
|
const string APP_JSON = "application/json";
|
||||||
|
public ApiCredentials Credentials => credentials;
|
||||||
|
|
||||||
|
public User CurrentUser
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
|
||||||
|
() => Get<User>("currentUser"));
|
||||||
|
if (user == null)
|
||||||
|
throw new Exception("Can't get current user");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateOffsets(UserQueueOffsets offsets)
|
||||||
|
{
|
||||||
|
Put($"/users/queue-offsets/set", new
|
||||||
|
{
|
||||||
|
Email = CurrentUser.Email,
|
||||||
|
Offsets = offsets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage Send(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_jwtToken))
|
||||||
|
Authorize();
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
|
||||||
|
var response = client.Send(request);
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
Authorize();
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
|
||||||
|
response = client.Send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
return response;
|
||||||
|
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var content = new StreamReader(stream).ReadToEnd();
|
||||||
|
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
|
||||||
|
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
|
||||||
|
}
|
||||||
|
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? Get<T>(string url)
|
||||||
|
{
|
||||||
|
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var json = new StreamReader(stream).ReadToEnd();
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Put<T>(string url, T obj)
|
||||||
|
{
|
||||||
|
Send(new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
|
{
|
||||||
|
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Authorize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
|
||||||
|
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
|
||||||
|
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
email = credentials.Email,
|
||||||
|
password = credentials.Password
|
||||||
|
};
|
||||||
|
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
|
||||||
|
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
|
||||||
|
var response = client.Send(message);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
|
||||||
|
|
||||||
|
var stream = response.Content.ReadAsStream();
|
||||||
|
var json = new StreamReader(stream).ReadToEnd();
|
||||||
|
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(result?.Token))
|
||||||
|
throw new Exception("JWT Token not found in response");
|
||||||
|
|
||||||
|
_jwtToken = result.Token;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Security;
|
|
||||||
using System.Text;
|
|
||||||
using Azaion.CommonSecurity.DTO;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
|
||||||
|
|
||||||
public class AzaionApiClient(HttpClient httpClient) : IDisposable
|
|
||||||
{
|
|
||||||
const string JSON_MEDIA = "application/json";
|
|
||||||
|
|
||||||
private string Email { get; set; } = null!;
|
|
||||||
private SecureString Password { get; set; } = new();
|
|
||||||
|
|
||||||
private string JwtToken { get; set; } = null!;
|
|
||||||
public User User { get; set; } = null!;
|
|
||||||
|
|
||||||
public static AzaionApiClient Create(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
ApiConfig apiConfig;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(SecurityConstants.CONFIG_PATH))
|
|
||||||
throw new FileNotFoundException(SecurityConstants.CONFIG_PATH);
|
|
||||||
var configStr = File.ReadAllText(SecurityConstants.CONFIG_PATH);
|
|
||||||
apiConfig = JsonConvert.DeserializeObject<SecureAppConfig>(configStr)!.ApiConfig;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
apiConfig = new ApiConfig
|
|
||||||
{
|
|
||||||
Url = SecurityConstants.DEFAULT_API_URL,
|
|
||||||
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT ,
|
|
||||||
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var api = new AzaionApiClient(new HttpClient
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(apiConfig.Url),
|
|
||||||
Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds)
|
|
||||||
});
|
|
||||||
|
|
||||||
api.EnterCredentials(credentials);
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnterCredentials(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password))
|
|
||||||
throw new Exception("Email or password is empty!");
|
|
||||||
|
|
||||||
Email = credentials.Email;
|
|
||||||
Password = credentials.Password.ToSecureString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Stream> GetResource(string fileName, string password, HardwareInfo hardware)
|
|
||||||
{
|
|
||||||
var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get")
|
|
||||||
{
|
|
||||||
Content = new StringContent(JsonConvert.SerializeObject(new { fileName, password, hardware }), Encoding.UTF8, JSON_MEDIA)
|
|
||||||
});
|
|
||||||
return await response.Content.ReadAsStreamAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Authorize()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Email) || Password.Length == 0)
|
|
||||||
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
|
|
||||||
|
|
||||||
var payload = new
|
|
||||||
{
|
|
||||||
email = Email,
|
|
||||||
password = Password.ToRealString()
|
|
||||||
};
|
|
||||||
var response = await httpClient.PostAsync(
|
|
||||||
"login",
|
|
||||||
new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, JSON_MEDIA));
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
|
|
||||||
|
|
||||||
var responseData = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
var result = JsonConvert.DeserializeObject<LoginResponse>(responseData);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(result?.Token))
|
|
||||||
throw new Exception("JWT Token not found in response");
|
|
||||||
|
|
||||||
var handler = new JwtSecurityTokenHandler();
|
|
||||||
var token = handler.ReadJwtToken(result.Token);
|
|
||||||
|
|
||||||
User = new User(token.Claims);
|
|
||||||
JwtToken = result.Token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> Send(HttpClient client, HttpRequestMessage request)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(JwtToken))
|
|
||||||
await Authorize();
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
|
|
||||||
var response = await client.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
await Authorize();
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
|
|
||||||
response = await client.SendAsync(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
return response;
|
|
||||||
|
|
||||||
var result = await response.Content.ReadAsStringAsync();
|
|
||||||
throw new Exception($"Failed: {response.StatusCode}! Result: {result}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
httpClient.Dispose();
|
|
||||||
Password.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using LazyCache;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.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);
|
||||||
|
}
|
||||||
@@ -8,101 +8,96 @@ namespace Azaion.CommonSecurity.Services;
|
|||||||
|
|
||||||
public interface IHardwareService
|
public interface IHardwareService
|
||||||
{
|
{
|
||||||
HardwareInfo GetHardware();
|
//HardwareInfo GetHardware();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HardwareService : IHardwareService
|
public class HardwareService : IHardwareService
|
||||||
{
|
{
|
||||||
private const string WIN32_GET_HARDWARE_COMMAND =
|
// private const string WIN32_GET_HARDWARE_COMMAND =
|
||||||
"wmic OS get TotalVisibleMemorySize /Value && " +
|
// "powershell -Command \"" +
|
||||||
"wmic CPU get Name /Value && " +
|
// "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " +
|
||||||
"wmic path Win32_VideoController get Name /Value";
|
// "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " +
|
||||||
|
// "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" +
|
||||||
|
// "\"";
|
||||||
|
//
|
||||||
|
// private const string UNIX_GET_HARDWARE_COMMAND =
|
||||||
|
// "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
|
||||||
|
// "lscpu | grep 'Model name:' | cut -d':' -f2 && " +
|
||||||
|
// "lspci | grep VGA | cut -d':' -f3\"";
|
||||||
|
|
||||||
private const string UNIX_GET_HARDWARE_COMMAND =
|
// public HardwareInfo GetHardware()
|
||||||
"/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
|
// {
|
||||||
"lscpu | grep 'Model name:' | cut -d':' -f2 && " +
|
// try
|
||||||
"lspci | grep VGA | cut -d':' -f3\"";
|
// {
|
||||||
|
// var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
|
||||||
|
// ? WIN32_GET_HARDWARE_COMMAND
|
||||||
|
// : UNIX_GET_HARDWARE_COMMAND);
|
||||||
|
//
|
||||||
|
// var lines = output
|
||||||
|
// .Replace("TotalVisibleMemorySize=", "")
|
||||||
|
// .Replace("Name=", "")
|
||||||
|
// .Replace(" ", " ")
|
||||||
|
// .Trim()
|
||||||
|
// .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
// .Select(x => x.Trim())
|
||||||
|
// .ToArray();
|
||||||
|
//
|
||||||
|
// if (lines.Length < 3)
|
||||||
|
// throw new Exception("Can't get hardware info");
|
||||||
|
//
|
||||||
|
// var hardwareInfo = new HardwareInfo
|
||||||
|
// {
|
||||||
|
// CPU = lines[0],
|
||||||
|
// GPU = lines[1],
|
||||||
|
// Memory = lines[2],
|
||||||
|
// MacAddress = GetMacAddress()
|
||||||
|
// };
|
||||||
|
// return hardwareInfo;
|
||||||
|
// }
|
||||||
|
// catch (Exception ex)
|
||||||
|
// {
|
||||||
|
// Console.WriteLine(ex.Message);
|
||||||
|
// throw;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
public HardwareInfo GetHardware()
|
// private string GetMacAddress()
|
||||||
{
|
// {
|
||||||
try
|
// var macAddress = NetworkInterface
|
||||||
{
|
// .GetAllNetworkInterfaces()
|
||||||
var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
|
// .Where(nic => nic.OperationalStatus == OperationalStatus.Up)
|
||||||
? WIN32_GET_HARDWARE_COMMAND
|
// .Select(nic => nic.GetPhysicalAddress().ToString())
|
||||||
: UNIX_GET_HARDWARE_COMMAND);
|
// .FirstOrDefault();
|
||||||
|
//
|
||||||
|
// return macAddress ?? string.Empty;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private string RunCommand(string command)
|
||||||
|
// {
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// using var process = new Process();
|
||||||
|
// process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
|
||||||
|
// process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
|
||||||
|
// ? $"-c \"{command}\""
|
||||||
|
// : $"/c {command}";
|
||||||
|
// process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
// process.StartInfo.UseShellExecute = false;
|
||||||
|
// process.StartInfo.CreateNoWindow = true;
|
||||||
|
//
|
||||||
|
// process.Start();
|
||||||
|
// var result = process.StandardOutput.ReadToEnd();
|
||||||
|
// process.WaitForExit();
|
||||||
|
//
|
||||||
|
// return result.Trim();
|
||||||
|
// }
|
||||||
|
// catch
|
||||||
|
// {
|
||||||
|
// return string.Empty;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
var lines = output
|
// private static string ToHash(string str) =>
|
||||||
.Replace("TotalVisibleMemorySize=", "")
|
// Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
||||||
.Replace("Name=", "")
|
|
||||||
.Replace(" ", " ")
|
|
||||||
.Trim()
|
|
||||||
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
var memoryStr = "Unknown RAM";
|
|
||||||
if (lines.Length > 0)
|
|
||||||
{
|
|
||||||
memoryStr = lines[0];
|
|
||||||
if (int.TryParse(memoryStr, out var memKb))
|
|
||||||
memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb";
|
|
||||||
}
|
|
||||||
|
|
||||||
var hardwareInfo = new HardwareInfo
|
|
||||||
{
|
|
||||||
Memory = memoryStr,
|
|
||||||
CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1])
|
|
||||||
? "Unknown RAM"
|
|
||||||
: lines[1],
|
|
||||||
GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2])
|
|
||||||
? "Unknown GPU"
|
|
||||||
: lines[2]
|
|
||||||
};
|
|
||||||
hardwareInfo.Hash = ToHash($"Azaion_{MacAddress()}_{hardwareInfo.CPU}_{hardwareInfo.GPU}");
|
|
||||||
return hardwareInfo;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex.Message);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string MacAddress()
|
|
||||||
{
|
|
||||||
var macAddress = NetworkInterface
|
|
||||||
.GetAllNetworkInterfaces()
|
|
||||||
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
|
|
||||||
.Select(nic => nic.GetPhysicalAddress().ToString())
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
return macAddress ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string RunCommand(string command)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var process = new Process();
|
|
||||||
process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
|
|
||||||
process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
|
|
||||||
? $"-c \"{command}\""
|
|
||||||
: $"/c {command}";
|
|
||||||
process.StartInfo.RedirectStandardOutput = true;
|
|
||||||
process.StartInfo.UseShellExecute = false;
|
|
||||||
process.StartInfo.CreateNoWindow = true;
|
|
||||||
|
|
||||||
process.Start();
|
|
||||||
var result = process.StandardOutput.ReadToEnd();
|
|
||||||
process.WaitForExit();
|
|
||||||
|
|
||||||
return result.Trim();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ToHash(string str) =>
|
|
||||||
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
|
using Azaion.CommonSecurity.DTO.Commands;
|
||||||
|
using MessagePack;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NetMQ;
|
||||||
|
using NetMQ.Sockets;
|
||||||
|
|
||||||
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
|
public interface IInferenceClient
|
||||||
|
{
|
||||||
|
void Send(RemoteCommand create);
|
||||||
|
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
|
||||||
|
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InferenceClient : IInferenceClient
|
||||||
|
{
|
||||||
|
private readonly DealerSocket _dealer = new();
|
||||||
|
private readonly Guid _clientId = Guid.NewGuid();
|
||||||
|
private readonly InferenceClientConfig _inferenceClientConfig;
|
||||||
|
|
||||||
|
public InferenceClient(IOptions<InferenceClientConfig> config)
|
||||||
|
{
|
||||||
|
_inferenceClientConfig = config.Value;
|
||||||
|
Start();
|
||||||
|
_ = Task.Run(ProcessClientCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
|
||||||
|
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
|
||||||
|
//RedirectStandardOutput = true,
|
||||||
|
//RedirectStandardError = true,
|
||||||
|
//CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||||
|
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
//throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
|
||||||
|
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
|
||||||
|
}
|
||||||
|
private async Task ProcessClientCommands()
|
||||||
|
{
|
||||||
|
//TODO: implement always on ready to client's requests. Utilize RemoteCommand
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_dealer.IsDisposed)
|
||||||
|
{
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
|
||||||
|
_dealer.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Send(RemoteCommand command)
|
||||||
|
{
|
||||||
|
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
|
||||||
|
{
|
||||||
|
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
|
||||||
|
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var tryNum = 0;
|
||||||
|
while (!ct.IsCancellationRequested && tryNum < retries)
|
||||||
|
{
|
||||||
|
tryNum++;
|
||||||
|
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ct.IsCancellationRequested)
|
||||||
|
throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +1,22 @@
|
|||||||
using System.Reflection;
|
using Azaion.CommonSecurity.DTO.Commands;
|
||||||
using Azaion.CommonSecurity.DTO;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
namespace Azaion.CommonSecurity.Services;
|
||||||
|
|
||||||
public interface IResourceLoader
|
public interface IResourceLoader
|
||||||
{
|
{
|
||||||
Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default);
|
MemoryStream LoadFile(string fileName, string? folder = null);
|
||||||
Assembly? LoadAssembly(string asmName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResourceLoader(AzaionApiClient api, ApiCredentials credentials) : IResourceLoader
|
public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader
|
||||||
{
|
{
|
||||||
private static readonly List<string> EncryptedResources =
|
public MemoryStream LoadFile(string fileName, string? folder = null)
|
||||||
[
|
|
||||||
"Azaion.Annotator",
|
|
||||||
"Azaion.Dataset"
|
|
||||||
];
|
|
||||||
|
|
||||||
public Assembly? LoadAssembly(string resourceName)
|
|
||||||
{
|
{
|
||||||
var assemblyName = resourceName.Split(',').First();
|
inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder)));
|
||||||
if (EncryptedResources.Contains(assemblyName))
|
var bytes = inferenceClient.GetBytes(2, 3);
|
||||||
{
|
if (bytes == null)
|
||||||
try
|
throw new Exception($"Unable to receive {fileName}");
|
||||||
{
|
|
||||||
var stream = Load($"{assemblyName}.dll").GetAwaiter().GetResult();
|
|
||||||
return Assembly.Load(stream.ToArray());
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e);
|
|
||||||
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
|
||||||
var dllPath = Path.Combine(currentLocation, "dummy", $"{assemblyName}.dll");
|
|
||||||
return Assembly.LoadFile(dllPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
return new MemoryStream(bytes);
|
||||||
.FirstOrDefault(a => a.GetName().Name == assemblyName);
|
|
||||||
|
|
||||||
return loadedAssembly;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
var hardwareService = new HardwareService();
|
|
||||||
var hardwareInfo = hardwareService.GetHardware();
|
|
||||||
|
|
||||||
var encryptedStream = Task.Run(() => api.GetResource(fileName, credentials.Password, hardwareInfo), cancellationToken).Result;
|
|
||||||
|
|
||||||
var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash);
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
await encryptedStream.DecryptTo(stream, key, cancellationToken);
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
return stream;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Security;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Azaion.CommonSecurity.Services;
|
|
||||||
|
|
||||||
public static class Security
|
|
||||||
{
|
|
||||||
private const int BUFFER_SIZE = 524288; // 512 KB buffer size
|
|
||||||
|
|
||||||
public static string ToHash(this string str) =>
|
|
||||||
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
|
||||||
|
|
||||||
public static string MakeEncryptionKey(string email, string password, string? hardwareHash) =>
|
|
||||||
$"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash();
|
|
||||||
|
|
||||||
public static SecureString ToSecureString(this string str)
|
|
||||||
{
|
|
||||||
var secureString = new SecureString();
|
|
||||||
foreach (var c in str.ToCharArray())
|
|
||||||
secureString.AppendChar(c);
|
|
||||||
|
|
||||||
return secureString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? ToRealString(this SecureString value)
|
|
||||||
{
|
|
||||||
var valuePtr = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
|
|
||||||
return Marshal.PtrToStringUni(valuePtr);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream));
|
|
||||||
if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key));
|
|
||||||
|
|
||||||
using var aes = Aes.Create();
|
|
||||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
|
||||||
aes.GenerateIV();
|
|
||||||
|
|
||||||
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
|
||||||
await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true);
|
|
||||||
|
|
||||||
// Prepend IV to the encrypted data
|
|
||||||
await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken);
|
|
||||||
|
|
||||||
var buffer = new byte[BUFFER_SIZE];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0)
|
|
||||||
await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
using var aes = Aes.Create();
|
|
||||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
|
||||||
|
|
||||||
// Read the IV from the start of the input stream
|
|
||||||
var iv = new byte[aes.BlockSize / 8];
|
|
||||||
_ = await encryptedStream.ReadAsync(iv, cancellationToken);
|
|
||||||
aes.IV = iv;
|
|
||||||
|
|
||||||
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
|
||||||
await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true);
|
|
||||||
|
|
||||||
// Read and write in chunks
|
|
||||||
var buffer = new byte[BUFFER_SIZE];
|
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0)
|
|
||||||
await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,23 +12,38 @@
|
|||||||
WindowState="Maximized">
|
WindowState="Maximized">
|
||||||
|
|
||||||
<Window.Resources>
|
<Window.Resources>
|
||||||
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationImageView}">
|
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationThumbnail}">
|
||||||
<Grid>
|
<Border BorderBrush="IndianRed" Padding="5">
|
||||||
<Grid.RowDefinitions>
|
<Border.Style>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<Style TargetType="Border">
|
||||||
<RowDefinition Height="32"></RowDefinition>
|
<Setter Property="BorderThickness" Value="0"></Setter>
|
||||||
</Grid.RowDefinitions>
|
<Style.Triggers>
|
||||||
<Image
|
<DataTrigger Binding="{Binding IsSeed}" Value="True">
|
||||||
Grid.Row="0"
|
<Setter Property="BorderThickness" Value="8"></Setter>
|
||||||
Source="{Binding Thumbnail}"
|
</DataTrigger>
|
||||||
Width="480"
|
<DataTrigger Binding="{Binding IsSeed}" Value="False">
|
||||||
Height="270"
|
<Setter Property="BorderThickness" Value="0"></Setter>
|
||||||
Margin="2" />
|
</DataTrigger>
|
||||||
<TextBlock
|
</Style.Triggers>
|
||||||
Grid.Row="1"
|
</Style>
|
||||||
Foreground="LightGray"
|
</Border.Style>
|
||||||
Text="{Binding ImageName}" />
|
<Grid>
|
||||||
</Grid>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="32"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Image
|
||||||
|
Grid.Row="0"
|
||||||
|
Source="{Binding Thumbnail}"
|
||||||
|
Width="480"
|
||||||
|
Height="270"
|
||||||
|
Margin="2" />
|
||||||
|
<TextBlock
|
||||||
|
Grid.Row="1"
|
||||||
|
Foreground="LightGray"
|
||||||
|
Text="{Binding ImageName}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
@@ -42,16 +57,13 @@
|
|||||||
<RowDefinition Height="32"></RowDefinition>
|
<RowDefinition Height="32"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="150" />
|
<ColumnDefinition Width="250" />
|
||||||
<ColumnDefinition Width="4"/>
|
<ColumnDefinition Width="4"/>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<controls:DetectionClasses
|
<controls:DetectionClasses x:Name="LvClasses"
|
||||||
x:Name="LvClasses"
|
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.Row="0">
|
Grid.Row="0" />
|
||||||
</controls:DetectionClasses>
|
|
||||||
|
|
||||||
<TabControl
|
<TabControl
|
||||||
Name="Switcher"
|
Name="Switcher"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
@@ -108,24 +120,80 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</StatusBar.ItemsPanel>
|
</StatusBar.ItemsPanel>
|
||||||
<StatusBarItem Grid.Column="2" Background="Black">
|
<StatusBarItem Grid.Column="0" Background="Black">
|
||||||
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock>
|
<Button Name="ValidateBtn"
|
||||||
|
Padding="2"
|
||||||
|
ToolTip="Підтвердити валідність. Клавіша: [V]"
|
||||||
|
Background="Black" BorderBrush="Black"
|
||||||
|
Cursor="Hand"
|
||||||
|
Click="ValidateAnnotationsClick">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image>
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m30.71 7.29-6-6a1 1 0 0 0 -.71-.29h-2v8a2 2 0 0 1 -2 2h-8a2 2 0 0
|
||||||
|
1 -2-2v-8h-6a3 3 0 0 0 -3 3v24a3 3 0 0 0 3 3h2v-9a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v9h2a3 3 0 0 0 3-3v-20a1 1 0 0 0 -.29-.71z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m12 1h8v8h-8z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="m23 21h-14a1 1 0 0 0 -1 1v9h16v-9a1 1 0 0 0 -1-1z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Text="Підтвердити валідність" Foreground="White" Padding="8 0 0 0"></TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
|
|
||||||
|
<Separator Grid.Column="1" />
|
||||||
|
|
||||||
|
<StatusBarItem x:Name="RefreshThumbnailsButtonItem" Grid.Column="2" Background="Black">
|
||||||
|
<Button
|
||||||
|
Padding="2"
|
||||||
|
Height="25"
|
||||||
|
ToolTip="Оновити базу іконок" Background="Black"
|
||||||
|
BorderBrush="Black"
|
||||||
|
Cursor="Hand"
|
||||||
|
Click="RefreshThumbnailsBtnClick">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image>
|
||||||
|
<Image.Source>
|
||||||
|
<DrawingImage>
|
||||||
|
<DrawingImage.Drawing>
|
||||||
|
<DrawingGroup ClipGeometry="M0,0 V1200 H1200 V0 H0 Z">
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="M889.68 166.32c-93.608-102.216-228.154-166.32-377.68-166.32-282.77 0-512
|
||||||
|
229.23-512 512h96c0-229.75 186.25-416 416-416 123.020 0 233.542 53.418 309.696 138.306l-149.696 149.694h352v-352l-134.32 134.32z" />
|
||||||
|
<GeometryDrawing Brush="LightGray" Geometry="M928 512c0 229.75-186.25 416-416 416-123.020
|
||||||
|
0-233.542-53.418-309.694-138.306l149.694-149.694h-352v352l134.32-134.32c93.608 102.216 228.154 166.32 377.68 166.32 282.77 0 512-229.23 512-512h-96z" />
|
||||||
|
</DrawingGroup>
|
||||||
|
</DrawingImage.Drawing>
|
||||||
|
</DrawingImage>
|
||||||
|
</Image.Source>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Foreground="White" Padding="8 0 0 0">Оновити базу іконок</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StatusBarItem>
|
||||||
|
<StatusBarItem Grid.Column="2" x:Name="RefreshProgressBarItem" Visibility="Hidden">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Name="RefreshThumbCaption" Padding="0 0 5 0">База іконок:</TextBlock>
|
||||||
|
<ProgressBar x:Name="RefreshThumbBar"
|
||||||
|
Width="150"
|
||||||
|
Height="15"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Background="#252525"
|
||||||
|
BorderBrush="#252525"
|
||||||
|
Foreground="LightBlue"
|
||||||
|
Maximum="100"
|
||||||
|
Minimum="0"
|
||||||
|
Value="0">
|
||||||
|
</ProgressBar>
|
||||||
|
</StackPanel>
|
||||||
|
</StatusBarItem>
|
||||||
|
|
||||||
<StatusBarItem Grid.Column="3" Background="Black">
|
<StatusBarItem Grid.Column="3" Background="Black">
|
||||||
<ProgressBar x:Name="RefreshThumbBar"
|
|
||||||
Width="150"
|
|
||||||
Height="15"
|
|
||||||
HorizontalAlignment="Stretch"
|
|
||||||
Background="#252525"
|
|
||||||
BorderBrush="#252525"
|
|
||||||
Foreground="LightBlue"
|
|
||||||
Maximum="100"
|
|
||||||
Minimum="0"
|
|
||||||
Value="0">
|
|
||||||
</ProgressBar>
|
|
||||||
</StatusBarItem>
|
|
||||||
<Separator Grid.Column="4"/>
|
|
||||||
<StatusBarItem Grid.Column="5" Background="Black">
|
|
||||||
<TextBlock Name="StatusText" Text=""/>
|
<TextBlock Name="StatusText" Text=""/>
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
</StatusBar>
|
</StatusBar>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using Azaion.Common;
|
|
||||||
using Azaion.Common.Database;
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
using Azaion.Common.DTO.Config;
|
||||||
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
|
using Azaion.CommonSecurity.DTO;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,26 +17,30 @@ using Color = ScottPlot.Color;
|
|||||||
|
|
||||||
namespace Azaion.Dataset;
|
namespace Azaion.Dataset;
|
||||||
|
|
||||||
public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent>
|
public partial class DatasetExplorer
|
||||||
{
|
{
|
||||||
private readonly ILogger<DatasetExplorer> _logger;
|
private readonly ILogger<DatasetExplorer> _logger;
|
||||||
private readonly AnnotationConfig _annotationConfig;
|
private readonly AnnotationConfig _annotationConfig;
|
||||||
private readonly DirectoriesConfig _directoriesConfig;
|
private readonly DirectoriesConfig _directoriesConfig;
|
||||||
|
|
||||||
private Dictionary<int, List<Annotation>> _annotationsDict;
|
private readonly Dictionary<int, Dictionary<string, Annotation>> _annotationsDict;
|
||||||
|
private readonly CancellationTokenSource _cts = new();
|
||||||
|
|
||||||
public ObservableCollection<AnnotationImageView> SelectedAnnotations { get; set; } = new();
|
public List<DetectionClass> AllDetectionClasses { get; set; }
|
||||||
private ObservableCollection<DetectionClass> AllAnnotationClasses { get; set; } = new();
|
public ObservableCollection<AnnotationThumbnail> SelectedAnnotations { get; set; } = new();
|
||||||
|
public readonly Dictionary<string, AnnotationThumbnail> SelectedAnnotationDict = new();
|
||||||
public Dictionary<string, LabelInfo> LabelsCache { get; set; } = new();
|
|
||||||
|
|
||||||
private int _tempSelectedClassIdx = 0;
|
private int _tempSelectedClassIdx = 0;
|
||||||
private readonly IGalleryService _galleryService;
|
private readonly IGalleryService _galleryService;
|
||||||
private readonly IDbFactory _dbFactory;
|
private readonly IDbFactory _dbFactory;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public readonly List<DetectionClass> AnnotationsClasses;
|
||||||
|
|
||||||
|
|
||||||
public bool ThumbnailLoading { get; set; }
|
public bool ThumbnailLoading { get; set; }
|
||||||
|
|
||||||
public AnnotationImageView? CurrentAnnotation { get; set; }
|
public AnnotationThumbnail? CurrentAnnotation { get; set; }
|
||||||
|
|
||||||
public DatasetExplorer(
|
public DatasetExplorer(
|
||||||
IOptions<DirectoriesConfig> directoriesConfig,
|
IOptions<DirectoriesConfig> directoriesConfig,
|
||||||
@@ -44,119 +48,114 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
ILogger<DatasetExplorer> logger,
|
ILogger<DatasetExplorer> logger,
|
||||||
IGalleryService galleryService,
|
IGalleryService galleryService,
|
||||||
FormState formState,
|
FormState formState,
|
||||||
IDbFactory dbFactory)
|
IDbFactory dbFactory,
|
||||||
|
IMediator mediator)
|
||||||
{
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
_directoriesConfig = directoriesConfig.Value;
|
_directoriesConfig = directoriesConfig.Value;
|
||||||
_annotationConfig = annotationConfig.Value;
|
_annotationConfig = annotationConfig.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_galleryService = galleryService;
|
_galleryService = galleryService;
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
|
_mediator = mediator;
|
||||||
|
|
||||||
|
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
|
||||||
|
_annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id))
|
||||||
|
.ToDictionary(x => x, _ => new Dictionary<string, Annotation>());
|
||||||
|
_annotationsDict.Add(-1, []);
|
||||||
|
|
||||||
|
AnnotationsClasses = annotationConfig.Value.DetectionClasses;
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
Loaded += OnLoaded;
|
Loaded += OnLoaded;
|
||||||
Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer;
|
Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer;
|
||||||
|
|
||||||
ThumbnailsView.KeyDown += async (sender, args) =>
|
ThumbnailsView.KeyDown += async (sender, args) =>
|
||||||
{
|
{
|
||||||
switch (args.Key)
|
switch (args.Key)
|
||||||
{
|
{
|
||||||
case Key.Delete:
|
case Key.Delete:
|
||||||
DeleteAnnotations();
|
await DeleteAnnotations();
|
||||||
break;
|
break;
|
||||||
case Key.Enter:
|
case Key.Enter:
|
||||||
await EditAnnotation();
|
await EditAnnotation(ThumbnailsView.SelectedIndex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation();
|
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(ThumbnailsView.SelectedIndex);
|
||||||
|
|
||||||
ThumbnailsView.SelectionChanged += (_, _) =>
|
ThumbnailsView.SelectionChanged += (_, _) =>
|
||||||
{
|
{
|
||||||
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
|
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
|
||||||
|
ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed)
|
||||||
|
? Visibility.Visible
|
||||||
|
: Visibility.Hidden;
|
||||||
};
|
};
|
||||||
|
ExplorerEditor.GetTimeFunc = () => CurrentAnnotation!.Annotation.Time;
|
||||||
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath);
|
_galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
|
||||||
galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
|
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
|
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
|
||||||
};
|
};
|
||||||
|
Closing += (_, _) => _cts.Cancel();
|
||||||
|
|
||||||
|
AllDetectionClasses = new List<DetectionClass>(
|
||||||
|
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
|
||||||
|
.Concat(_annotationConfig.DetectionClasses));
|
||||||
|
LvClasses.Init(AllDetectionClasses);
|
||||||
|
|
||||||
|
_dbFactory.Run(async db =>
|
||||||
|
{
|
||||||
|
var allAnnotations = await db.Annotations
|
||||||
|
.LoadWith(x => x.Detections)
|
||||||
|
.OrderBy(x => x.AnnotationStatus)
|
||||||
|
.ThenByDescending(x => x.CreatedDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var annotation in allAnnotations)
|
||||||
|
AddAnnotationToDict(annotation);
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
DataContext = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
AllAnnotationClasses = new ObservableCollection<DetectionClass>(
|
LvClasses.DetectionClassChanged += async (_, args) =>
|
||||||
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
|
|
||||||
.Concat(_annotationConfig.AnnotationClasses));
|
|
||||||
LvClasses.ItemsSource = AllAnnotationClasses;
|
|
||||||
|
|
||||||
LvClasses.MouseUp += async (_, _) =>
|
|
||||||
{
|
{
|
||||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
ExplorerEditor.CurrentAnnClass = args.DetectionClass;
|
||||||
ExplorerEditor.CurrentAnnClass = selectedClass;
|
|
||||||
_annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
|
|
||||||
|
|
||||||
if (Switcher.SelectedIndex == 0)
|
if (Switcher.SelectedIndex == 0)
|
||||||
await ReloadThumbnails();
|
await ReloadThumbnails();
|
||||||
else
|
else
|
||||||
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
|
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
|
||||||
ann.DetectionClass = selectedClass;
|
ann.DetectionClass = args.DetectionClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
LvClasses.SelectionChanged += (_, _) =>
|
ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First();
|
||||||
{
|
|
||||||
if (Switcher.SelectedIndex != 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
|
|
||||||
if (selectedClass == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ExplorerEditor.CurrentAnnClass = selectedClass;
|
|
||||||
|
|
||||||
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
|
|
||||||
ann.DetectionClass = selectedClass;
|
|
||||||
};
|
|
||||||
|
|
||||||
LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
|
|
||||||
ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.SelectedItem;
|
|
||||||
|
|
||||||
await _dbFactory.Run(async db =>
|
|
||||||
{
|
|
||||||
var allAnnotations = await db.Annotations
|
|
||||||
.LoadWith(x => x.Detections)
|
|
||||||
.OrderByDescending(x => x.CreatedDate)
|
|
||||||
.ToListAsync();
|
|
||||||
_annotationsDict = AllAnnotationClasses.ToDictionary(x => x.Id, _ => new List<Annotation>());
|
|
||||||
|
|
||||||
foreach (var annotation in allAnnotations)
|
|
||||||
AddAnnotationToDict(annotation);
|
|
||||||
});
|
|
||||||
await ReloadThumbnails();
|
await ReloadThumbnails();
|
||||||
await LoadClassDistribution();
|
await LoadClassDistribution();
|
||||||
|
|
||||||
RefreshThumbBar.Value = _galleryService.ProcessedThumbnailsPercentage;
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddAnnotationToDict(Annotation annotation)
|
public void AddAnnotationToDict(Annotation annotation)
|
||||||
{
|
{
|
||||||
foreach (var c in annotation.Classes)
|
foreach (var c in annotation.Classes)
|
||||||
_annotationsDict[c].Add(annotation);
|
_annotationsDict[c][annotation.Name] = annotation;
|
||||||
_annotationsDict[-1].Add(annotation);
|
_annotationsDict[-1][annotation.Name] = annotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadClassDistribution()
|
private async Task LoadClassDistribution()
|
||||||
{
|
{
|
||||||
var data = LabelsCache
|
var data = _annotationsDict
|
||||||
.SelectMany(x => x.Value.Classes)
|
.Where(x => x.Key != -1)
|
||||||
.GroupBy(x => x)
|
.Select(gr => new
|
||||||
.Select(x => new
|
|
||||||
{
|
{
|
||||||
x.Key,
|
gr.Key,
|
||||||
_annotationConfig.DetectionClassesDict[x.Key].Name,
|
_annotationConfig.DetectionClassesDict[gr.Key].ShortName,
|
||||||
_annotationConfig.DetectionClassesDict[x.Key].Color,
|
_annotationConfig.DetectionClassesDict[gr.Key].Color,
|
||||||
ClassCount = x.Count()
|
ClassCount = gr.Value.Count
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -177,7 +176,7 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
|
|
||||||
foreach (var x in data)
|
foreach (var x in data)
|
||||||
{
|
{
|
||||||
var label = ClassDistribution.Plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1);
|
var label = ClassDistribution.Plot.Add.Text(x.ShortName, 50, -1.5 * x.Key + 1.1);
|
||||||
label.LabelFontColor = foregroundColor;
|
label.LabelFontColor = foregroundColor;
|
||||||
label.LabelFontSize = 18;
|
label.LabelFontSize = 18;
|
||||||
}
|
}
|
||||||
@@ -187,28 +186,36 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
|
ClassDistribution.Plot.FigureBackground.Color = new("#888888");
|
||||||
|
|
||||||
ClassDistribution.Refresh();
|
ClassDistribution.Refresh();
|
||||||
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e)
|
private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
|
RefreshThumbnailsButtonItem.Visibility = Visibility.Hidden;
|
||||||
|
RefreshProgressBarItem.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
var result = MessageBox.Show($"Видалити всі іконки та згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
|
||||||
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
if (result != MessageBoxResult.Yes)
|
if (result != MessageBoxResult.Yes)
|
||||||
return;
|
return;
|
||||||
_galleryService.ClearThumbnails();
|
await _galleryService.ClearThumbnails();
|
||||||
_galleryService.RefreshThumbnails();
|
await _galleryService.RefreshThumbnails();
|
||||||
|
|
||||||
|
RefreshProgressBarItem.Visibility = Visibility.Hidden;
|
||||||
|
RefreshThumbnailsButtonItem.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EditAnnotation()
|
public async Task EditAnnotation(int index)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ThumbnailLoading = true;
|
ThumbnailLoading = true;
|
||||||
|
if (index == -1)
|
||||||
if (ThumbnailsView.SelectedItem == null)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!;
|
CurrentAnnotation = (ThumbnailsView.Items[index] as AnnotationThumbnail)!;
|
||||||
|
ThumbnailsView.SelectedIndex = index;
|
||||||
|
|
||||||
var ann = CurrentAnnotation.Annotation;
|
var ann = CurrentAnnotation.Annotation;
|
||||||
ExplorerEditor.Background = new ImageBrush
|
ExplorerEditor.Background = new ImageBrush
|
||||||
{
|
{
|
||||||
@@ -216,16 +223,9 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
};
|
};
|
||||||
SwitchTab(toEditor: true);
|
SwitchTab(toEditor: true);
|
||||||
|
|
||||||
var time = Constants.GetTime(ann.ImagePath);
|
var time = ann.Time;
|
||||||
ExplorerEditor.RemoveAllAnns();
|
ExplorerEditor.RemoveAllAnns();
|
||||||
foreach (var deetection in ann.Detections)
|
ExplorerEditor.CreateDetections(time, ann.Detections, _annotationConfig.DetectionClasses, ExplorerEditor.RenderSize);
|
||||||
{
|
|
||||||
var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber];
|
|
||||||
var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
|
|
||||||
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
ThumbnailLoading = false;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -234,7 +234,11 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ThumbnailLoading = false;
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
ThumbnailLoading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -245,58 +249,63 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
|
|||||||
{
|
{
|
||||||
AnnotationsTab.Visibility = Visibility.Collapsed;
|
AnnotationsTab.Visibility = Visibility.Collapsed;
|
||||||
EditorTab.Visibility = Visibility.Visible;
|
EditorTab.Visibility = Visibility.Visible;
|
||||||
_tempSelectedClassIdx = LvClasses.SelectedIndex;
|
_tempSelectedClassIdx = LvClasses.CurrentClassNumber;
|
||||||
LvClasses.ItemsSource = _annotationConfig.AnnotationClasses;
|
LvClasses.DetectionDataGrid.ItemsSource = _annotationConfig.DetectionClasses;
|
||||||
|
|
||||||
Switcher.SelectedIndex = 1;
|
Switcher.SelectedIndex = 1;
|
||||||
LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1);
|
LvClasses.SelectNum(Math.Max(0, _tempSelectedClassIdx - 1));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AnnotationsTab.Visibility = Visibility.Visible;
|
AnnotationsTab.Visibility = Visibility.Visible;
|
||||||
EditorTab.Visibility = Visibility.Collapsed;
|
EditorTab.Visibility = Visibility.Collapsed;
|
||||||
LvClasses.ItemsSource = AllAnnotationClasses;
|
LvClasses.DetectionDataGrid.ItemsSource = AllDetectionClasses;
|
||||||
LvClasses.SelectedIndex = _tempSelectedClassIdx;
|
LvClasses.SelectNum(_tempSelectedClassIdx);
|
||||||
Switcher.SelectedIndex = 0;
|
Switcher.SelectedIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteAnnotations()
|
public async Task DeleteAnnotations()
|
||||||
{
|
{
|
||||||
var tempSelected = ThumbnailsView.SelectedIndex;
|
var tempSelected = ThumbnailsView.SelectedIndex;
|
||||||
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||||
if (result != MessageBoxResult.Yes)
|
if (result != MessageBoxResult.Yes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var selected = ThumbnailsView.SelectedItems.Count;
|
var annotations = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation)
|
||||||
for (var i = 0; i < selected; i++)
|
.ToList();
|
||||||
{
|
|
||||||
var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!;
|
await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
|
||||||
dto.Delete();
|
|
||||||
SelectedAnnotations.Remove(dto);
|
|
||||||
}
|
|
||||||
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
|
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadThumbnails()
|
private async Task ReloadThumbnails()
|
||||||
{
|
{
|
||||||
SelectedAnnotations.Clear();
|
SelectedAnnotations.Clear();
|
||||||
foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id])
|
SelectedAnnotationDict.Clear();
|
||||||
SelectedAnnotations.Add(new AnnotationImageView(ann));
|
var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId];
|
||||||
|
foreach (var ann in annotations.OrderByDescending(x => x.Value.CreatedDate))
|
||||||
|
{
|
||||||
|
var annThumb = new AnnotationThumbnail(ann.Value);
|
||||||
|
SelectedAnnotations.Add(annThumb);
|
||||||
|
SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void ValidateAnnotationsClick(object sender, RoutedEventArgs e)
|
||||||
private void AddThumbnail(Annotation annotation)
|
|
||||||
{
|
{
|
||||||
var selectedClass = ((DetectionClass?)LvClasses.SelectedItem)?.Id;
|
var result = MessageBox.Show("Підтверджуєте валідність обраних аннотацій?","Підтвердження валідності", MessageBoxButton.OKCancel, MessageBoxImage.Question);
|
||||||
if (selectedClass == null)
|
if (result != MessageBoxResult.OK)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
AddAnnotationToDict(annotation);
|
try
|
||||||
if (annotation.Classes.Contains(selectedClass.Value))
|
{
|
||||||
SelectedAnnotations.Add(new AnnotationImageView(annotation));
|
await _mediator.Publish(new DatasetExplorerControlEvent(PlaybackControlEnum.ValidateAnnotations), _cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) =>
|
|
||||||
AddThumbnail(notification.Annotation);
|
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,40 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Azaion.Common.Database;
|
||||||
using Azaion.Common.DTO;
|
using Azaion.Common.DTO;
|
||||||
using Azaion.Common.DTO.Config;
|
|
||||||
using Azaion.Common.DTO.Queue;
|
using Azaion.Common.DTO.Queue;
|
||||||
|
using Azaion.Common.Events;
|
||||||
using Azaion.Common.Services;
|
using Azaion.Common.Services;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Azaion.Dataset;
|
namespace Azaion.Dataset;
|
||||||
|
|
||||||
public class DatasetExplorerEventHandler(
|
public class DatasetExplorerEventHandler(
|
||||||
DatasetExplorer datasetExplorer,
|
DatasetExplorer datasetExplorer,
|
||||||
AnnotationService annotationService) : INotificationHandler<KeyEvent>
|
AnnotationService annotationService) :
|
||||||
|
INotificationHandler<KeyEvent>,
|
||||||
|
INotificationHandler<DatasetExplorerControlEvent>,
|
||||||
|
INotificationHandler<AnnotationCreatedEvent>,
|
||||||
|
INotificationHandler<AnnotationsDeletedEvent>
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
|
||||||
{
|
{
|
||||||
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
|
{ Key.Enter, PlaybackControlEnum.SaveAnnotations },
|
||||||
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
|
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
|
||||||
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
|
{ Key.X, PlaybackControlEnum.RemoveAllAnns },
|
||||||
{ Key.Escape, PlaybackControlEnum.Close }
|
{ Key.Escape, PlaybackControlEnum.Close },
|
||||||
|
{ Key.Down, PlaybackControlEnum.Next },
|
||||||
|
{ Key.Up, PlaybackControlEnum.Previous },
|
||||||
|
{ Key.V, PlaybackControlEnum.ValidateAnnotations},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await HandleControl(notification.PlaybackControl, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
|
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (keyEvent.WindowEnum != WindowEnum.DatasetExplorer)
|
if (keyEvent.WindowEnum != WindowEnum.DatasetExplorer)
|
||||||
@@ -33,15 +47,15 @@ public class DatasetExplorerEventHandler(
|
|||||||
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1;
|
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1;
|
||||||
|
|
||||||
if (keyNumber.HasValue)
|
if (keyNumber.HasValue)
|
||||||
datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value;
|
datasetExplorer.LvClasses.SelectNum(keyNumber.Value);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
|
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
|
||||||
await HandleControl(value);
|
await HandleControl(value, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleControl(PlaybackControlEnum controlEnum)
|
private async Task HandleControl(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
switch (controlEnum)
|
switch (controlEnum)
|
||||||
{
|
{
|
||||||
@@ -49,24 +63,91 @@ public class DatasetExplorerEventHandler(
|
|||||||
if (datasetExplorer.ThumbnailLoading)
|
if (datasetExplorer.ThumbnailLoading)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var fName = Path.GetFileNameWithoutExtension(datasetExplorer.CurrentAnnotation!.Annotation.ImagePath);
|
var a = datasetExplorer.CurrentAnnotation!.Annotation;
|
||||||
var extension = Path.GetExtension(fName);
|
|
||||||
|
|
||||||
var detections = datasetExplorer.ExplorerEditor.CurrentDetections
|
var detections = datasetExplorer.ExplorerEditor.CurrentDetections
|
||||||
.Select(x => new Detection(fName, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize)))
|
.Select(x => new Detection(a.Name, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize)))
|
||||||
.ToList();
|
.ToList();
|
||||||
await annotationService.SaveAnnotation(fName, extension, detections, SourceEnum.Manual);
|
var index = datasetExplorer.ThumbnailsView.SelectedIndex;
|
||||||
datasetExplorer.SwitchTab(toEditor: false);
|
await annotationService.SaveAnnotation(a.OriginalMediaName, a.Time, detections, token: cancellationToken);
|
||||||
|
await datasetExplorer.EditAnnotation(index + 1);
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.RemoveSelectedAnns:
|
case PlaybackControlEnum.RemoveSelectedAnns:
|
||||||
datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
|
if (datasetExplorer.ExplorerEditor.CurrentDetections.Any(x => x.IsSelected))
|
||||||
|
datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await datasetExplorer.DeleteAnnotations();
|
||||||
|
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case PlaybackControlEnum.RemoveAllAnns:
|
case PlaybackControlEnum.RemoveAllAnns:
|
||||||
datasetExplorer.ExplorerEditor.RemoveAllAnns();
|
datasetExplorer.ExplorerEditor.RemoveAllAnns();
|
||||||
break;
|
break;
|
||||||
|
case PlaybackControlEnum.Next:
|
||||||
|
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex + 1);
|
||||||
|
break;
|
||||||
|
case PlaybackControlEnum.Previous:
|
||||||
|
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex - 1);
|
||||||
|
break;
|
||||||
case PlaybackControlEnum.Close:
|
case PlaybackControlEnum.Close:
|
||||||
datasetExplorer.SwitchTab(toEditor: false);
|
datasetExplorer.SwitchTab(toEditor: false);
|
||||||
break;
|
break;
|
||||||
|
case PlaybackControlEnum.ValidateAnnotations:
|
||||||
|
var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>()
|
||||||
|
.Select(x => x.Annotation)
|
||||||
|
.ToList();
|
||||||
|
await annotationService.ValidateAnnotations(annotations, cancellationToken);
|
||||||
|
foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation)))
|
||||||
|
{
|
||||||
|
ann.Annotation.AnnotationStatus = AnnotationStatus.Validated;
|
||||||
|
if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value))
|
||||||
|
value.Annotation.AnnotationStatus = AnnotationStatus.Validated;
|
||||||
|
ann.UpdateUI();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
datasetExplorer.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var annotation = notification.Annotation;
|
||||||
|
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
|
||||||
|
|
||||||
|
//TODO: For editing existing need to handle updates
|
||||||
|
datasetExplorer.AddAnnotationToDict(annotation);
|
||||||
|
if (annotation.Classes.Contains(selectedClass) || selectedClass == -1)
|
||||||
|
{
|
||||||
|
var annThumb = new AnnotationThumbnail(annotation);
|
||||||
|
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
|
||||||
|
{
|
||||||
|
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
|
||||||
|
var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name);
|
||||||
|
if (ann != null)
|
||||||
|
datasetExplorer.SelectedAnnotations.Remove(ann);
|
||||||
|
}
|
||||||
|
|
||||||
|
datasetExplorer.SelectedAnnotations.Insert(0, annThumb);
|
||||||
|
datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var names = notification.Annotations.Select(x => x.Name).ToList();
|
||||||
|
var annThumbs = datasetExplorer.SelectedAnnotationDict
|
||||||
|
.Where(x => names.Contains(x.Key))
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToList();
|
||||||
|
foreach (var annThumb in annThumbs)
|
||||||
|
{
|
||||||
|
datasetExplorer.SelectedAnnotations.Remove(annThumb);
|
||||||
|
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<h2>Azaion AI</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Azaion AI is a worker written on cython (c-compilable python) which listens to socket and rabbit queue.
|
||||||
|
It accepts commands om a format:
|
||||||
|
|
||||||
|
- CommandType: Inference / Load
|
||||||
|
- Filename
|
||||||
|
|
||||||
|
And correspondingly do inference or just load encrypted file from the API.
|
||||||
|
Results (file or annotations) is putted to the other queue, or the same socket, depending on the command source.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Installation</h2>
|
||||||
|
|
||||||
|
Prepare correct onnx model from YOLO:
|
||||||
|
```python
|
||||||
|
from ultralytics import YOLO
|
||||||
|
import netron
|
||||||
|
|
||||||
|
model = YOLO("azaion.pt")
|
||||||
|
model.export(format="onnx", imgsz=1280, nms=True, batch=4)
|
||||||
|
netron.start('azaion.onnx')
|
||||||
|
```
|
||||||
|
Read carefully about [export arguments](https://docs.ultralytics.com/modes/export/), you have to use nms=True, and batching with a proper batch size
|
||||||
|
|
||||||
|
<h3>Install libs</h3>
|
||||||
|
https://www.python.org/downloads/
|
||||||
|
|
||||||
|
Windows
|
||||||
|
|
||||||
|
- [Install CUDA](https://developer.nvidia.com/cuda-12-1-0-download-archive)
|
||||||
|
|
||||||
|
Linux
|
||||||
|
```
|
||||||
|
sudo apt install nvidia-driver-535
|
||||||
|
|
||||||
|
wget https://developer.download.nvidia.com/compute/cudnn/9.2.0/local_installers/cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb
|
||||||
|
sudo dpkg -i cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb
|
||||||
|
|
||||||
|
sudo cp /var/cudnn-local-repo-ubuntu2204-9.2.0/cudnn-*-keyring.gpg /usr/share/keyrings/
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get -y install cudnn nvidia-cuda-toolkit -y
|
||||||
|
nvcc --version
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Install dependencies</h3>
|
||||||
|
1. Install python with max version 3.11. Pytorch for now supports 3.11 max
|
||||||
|
|
||||||
|
Make sure that your virtual env is installed with links to the global python packages and headers, like this:
|
||||||
|
```
|
||||||
|
python -m venv --system-site-packages venv
|
||||||
|
```
|
||||||
|
This is crucial for the build because build needs Python.h header and other files.
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requirements.txt
|
||||||
|
pip install pyinstaller
|
||||||
|
|
||||||
|
```
|
||||||
|
In case of fbgemm.dll error (Windows specific):
|
||||||
|
|
||||||
|
- copypaste libomp140.x86_64.dll to C:\Windows\System32
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Build</h3>
|
||||||
|
```
|
||||||
|
python setup.py build_ext --inplace
|
||||||
|
```
|
||||||
|
|
||||||
|
<h3>Build exe</h3>
|
||||||
|
```
|
||||||
|
.\build.cmd
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
cdef class AIRecognitionConfig:
|
||||||
|
cdef public double frame_recognition_seconds
|
||||||
|
cdef public int frame_period_recognition
|
||||||
|
cdef public double probability_threshold
|
||||||
|
|
||||||
|
cdef public double tracking_distance_confidence
|
||||||
|
cdef public double tracking_probability_increase
|
||||||
|
cdef public double tracking_intersection_threshold
|
||||||
|
|
||||||
|
cdef public bytes file_data
|
||||||
|
cdef public list[str] paths
|
||||||
|
cdef public int model_batch_size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
from msgpack import unpackb
|
||||||
|
|
||||||
|
cdef class AIRecognitionConfig:
|
||||||
|
def __init__(self,
|
||||||
|
frame_period_recognition,
|
||||||
|
frame_recognition_seconds,
|
||||||
|
probability_threshold,
|
||||||
|
|
||||||
|
tracking_distance_confidence,
|
||||||
|
tracking_probability_increase,
|
||||||
|
tracking_intersection_threshold,
|
||||||
|
|
||||||
|
file_data,
|
||||||
|
paths,
|
||||||
|
model_batch_size
|
||||||
|
):
|
||||||
|
self.frame_period_recognition = frame_period_recognition
|
||||||
|
self.frame_recognition_seconds = frame_recognition_seconds
|
||||||
|
self.probability_threshold = probability_threshold
|
||||||
|
|
||||||
|
self.tracking_distance_confidence = tracking_distance_confidence
|
||||||
|
self.tracking_probability_increase = tracking_probability_increase
|
||||||
|
self.tracking_intersection_threshold = tracking_intersection_threshold
|
||||||
|
|
||||||
|
self.file_data = file_data
|
||||||
|
self.paths = paths
|
||||||
|
self.model_batch_size = model_batch_size
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, '
|
||||||
|
f'probability_increase : {self.tracking_probability_increase}, '
|
||||||
|
f'intersection_threshold : {self.tracking_intersection_threshold}, '
|
||||||
|
f'frame_period_recognition : {self.frame_period_recognition}, '
|
||||||
|
f'paths: {self.paths}, '
|
||||||
|
f'model_batch_size: {self.model_batch_size}')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data):
|
||||||
|
unpacked = unpackb(data, strict_map_key=False)
|
||||||
|
return AIRecognitionConfig(
|
||||||
|
unpacked.get("f_pr", 0),
|
||||||
|
unpacked.get("f_rs", 0.0),
|
||||||
|
unpacked.get("pt", 0.0),
|
||||||
|
|
||||||
|
unpacked.get("t_dc", 0.0),
|
||||||
|
unpacked.get("t_pi", 0.0),
|
||||||
|
unpacked.get("t_it", 0.0),
|
||||||
|
|
||||||
|
unpacked.get("d", b''),
|
||||||
|
unpacked.get("p", []),
|
||||||
|
unpacked.get("m_bs")
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cdef class Detection:
|
||||||
|
cdef public double x, y, w, h, confidence
|
||||||
|
cdef public str annotation_name
|
||||||
|
cdef public int cls
|
||||||
|
|
||||||
|
cdef public overlaps(self, Detection det2)
|
||||||
|
|
||||||
|
cdef class Annotation:
|
||||||
|
cdef public str name
|
||||||
|
cdef public str original_media_name
|
||||||
|
cdef long time
|
||||||
|
cdef public list[Detection] detections
|
||||||
|
cdef public bytes image
|
||||||
|
|
||||||
|
cdef format_time(self, ms)
|
||||||
|
cdef bytes serialize(self)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import msgpack
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
cdef class Detection:
|
||||||
|
def __init__(self, double x, double y, double w, double h, int cls, double confidence):
|
||||||
|
self.annotation_name = None
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
self.cls = cls
|
||||||
|
self.confidence = confidence
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.cls}: {self.x:.2f} {self.y:.2f} {self.w:.2f} {self.h:.2f}, prob: {(self.confidence*100):.1f}%'
|
||||||
|
|
||||||
|
cdef overlaps(self, Detection det2):
|
||||||
|
cdef double overlap_x = 0.5 * (self.w + det2.w) - abs(self.x - det2.x)
|
||||||
|
cdef double overlap_y = 0.5 * (self.h + det2.h) - abs(self.y - det2.y)
|
||||||
|
cdef double overlap_area = max(0.0, overlap_x) * max(0.0, overlap_y)
|
||||||
|
cdef double min_area = min(self.w * self.h, det2.w * det2.h)
|
||||||
|
|
||||||
|
return overlap_area / min_area > 0.6
|
||||||
|
|
||||||
|
cdef class Annotation:
|
||||||
|
def __init__(self, str name, long ms, list[Detection] detections):
|
||||||
|
self.original_media_name = Path(<str>name).stem.replace(" ", "")
|
||||||
|
self.name = f'{self.original_media_name}_{self.format_time(ms)}'
|
||||||
|
self.time = ms
|
||||||
|
self.detections = detections if detections is not None else []
|
||||||
|
for d in self.detections:
|
||||||
|
d.annotation_name = self.name
|
||||||
|
self.image = b''
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if not self.detections:
|
||||||
|
return f"{self.name}: No detections"
|
||||||
|
|
||||||
|
detections_str = ", ".join(
|
||||||
|
f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})"
|
||||||
|
for d in self.detections
|
||||||
|
)
|
||||||
|
return f"{self.name}: {detections_str}"
|
||||||
|
|
||||||
|
cdef format_time(self, ms):
|
||||||
|
# Calculate hours, minutes, seconds, and hundreds of milliseconds.
|
||||||
|
h = ms // 3600000 # Total full hours.
|
||||||
|
ms_remaining = ms % 3600000
|
||||||
|
m = ms_remaining // 60000 # Full minutes.
|
||||||
|
ms_remaining %= 60000
|
||||||
|
s = ms_remaining // 1000 # Full seconds.
|
||||||
|
f = (ms_remaining % 1000) // 100 # Hundreds of milliseconds.
|
||||||
|
h = h % 10
|
||||||
|
return f"{h}{m:02}{s:02}{f}"
|
||||||
|
|
||||||
|
cdef bytes serialize(self):
|
||||||
|
return msgpack.packb({
|
||||||
|
"n": self.name,
|
||||||
|
"mn": self.original_media_name,
|
||||||
|
"i": self.image, # "i" = image
|
||||||
|
"t": self.time, # "t" = time
|
||||||
|
"d": [ # "d" = detections
|
||||||
|
{
|
||||||
|
"an": det.annotation_name,
|
||||||
|
"x": det.x,
|
||||||
|
"y": det.y,
|
||||||
|
"w": det.w,
|
||||||
|
"h": det.h,
|
||||||
|
"c": det.cls,
|
||||||
|
"p": det.confidence
|
||||||
|
} for det in self.detections
|
||||||
|
]
|
||||||
|
})
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from user cimport User
|
||||||
|
from credentials cimport Credentials
|
||||||
|
from cdn_manager cimport CDNManager
|
||||||
|
|
||||||
|
|
||||||
|
cdef class ApiClient:
|
||||||
|
cdef Credentials credentials
|
||||||
|
cdef CDNManager cdn_manager
|
||||||
|
cdef str token, folder, api_url
|
||||||
|
cdef User user
|
||||||
|
|
||||||
|
cdef set_credentials(self, Credentials credentials)
|
||||||
|
cdef login(self)
|
||||||
|
cdef set_token(self, str token)
|
||||||
|
cdef get_user(self)
|
||||||
|
|
||||||
|
cdef load_bytes(self, str filename, str folder)
|
||||||
|
cdef upload_file(self, str filename, bytes resource, str folder)
|
||||||
|
cdef load_big_small_resource(self, str resource_name, str folder, str key)
|
||||||
|
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import json
|
||||||
|
from http import HTTPStatus
|
||||||
|
from os import path
|
||||||
|
from uuid import UUID
|
||||||
|
import jwt
|
||||||
|
import requests
|
||||||
|
cimport constants
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from cdn_manager cimport CDNManager, CDNCredentials
|
||||||
|
from hardware_service cimport HardwareService
|
||||||
|
from security cimport Security
|
||||||
|
from user cimport User, RoleEnum
|
||||||
|
|
||||||
|
cdef class ApiClient:
|
||||||
|
"""Handles API authentication and downloading of the AI model."""
|
||||||
|
def __init__(self, str api_url):
|
||||||
|
self.credentials = None
|
||||||
|
self.user = None
|
||||||
|
self.token = None
|
||||||
|
self.cdn_manager = None
|
||||||
|
self.api_url = api_url
|
||||||
|
|
||||||
|
cdef set_credentials(self, Credentials credentials):
|
||||||
|
self.credentials = credentials
|
||||||
|
yaml_bytes = self.load_bytes(constants.CDN_CONFIG, <str>'')
|
||||||
|
yaml_config = yaml.safe_load(yaml_bytes)
|
||||||
|
creds = CDNCredentials(yaml_config["host"],
|
||||||
|
yaml_config["downloader_access_key"],
|
||||||
|
yaml_config["downloader_access_secret"],
|
||||||
|
yaml_config["uploader_access_key"],
|
||||||
|
yaml_config["uploader_access_secret"])
|
||||||
|
|
||||||
|
self.cdn_manager = CDNManager(creds)
|
||||||
|
|
||||||
|
cdef login(self):
|
||||||
|
response = requests.post(f"{self.api_url}/login",
|
||||||
|
json={"email": self.credentials.email, "password": self.credentials.password})
|
||||||
|
response.raise_for_status()
|
||||||
|
token = response.json()["token"]
|
||||||
|
self.set_token(token)
|
||||||
|
|
||||||
|
|
||||||
|
cdef set_token(self, str token):
|
||||||
|
self.token = token
|
||||||
|
claims = jwt.decode(token, options={"verify_signature": False})
|
||||||
|
|
||||||
|
try:
|
||||||
|
id = str(UUID(claims.get("nameid", "")))
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("Invalid GUID format in claims")
|
||||||
|
|
||||||
|
email = claims.get("unique_name", "")
|
||||||
|
|
||||||
|
role_str = claims.get("role", "")
|
||||||
|
if role_str == "ApiAdmin":
|
||||||
|
role = RoleEnum.ApiAdmin
|
||||||
|
elif role_str == "Admin":
|
||||||
|
role = RoleEnum.Admin
|
||||||
|
elif role_str == "ResourceUploader":
|
||||||
|
role = RoleEnum.ResourceUploader
|
||||||
|
elif role_str == "Validator":
|
||||||
|
role = RoleEnum.Validator
|
||||||
|
elif role_str == "Operator":
|
||||||
|
role = RoleEnum.Operator
|
||||||
|
else:
|
||||||
|
role = RoleEnum.NONE
|
||||||
|
self.user = User(id, email, role)
|
||||||
|
|
||||||
|
cdef get_user(self):
|
||||||
|
if self.user is None:
|
||||||
|
self.login()
|
||||||
|
return self.user
|
||||||
|
|
||||||
|
cdef upload_file(self, str filename, bytes resource, str folder):
|
||||||
|
if self.token is None:
|
||||||
|
self.login()
|
||||||
|
url = f"{self.api_url}/resources/{folder}"
|
||||||
|
headers = { "Authorization": f"Bearer {self.token}" }
|
||||||
|
files = {'data': (filename, resource)}
|
||||||
|
try:
|
||||||
|
r = requests.post(url, headers=headers, files=files, allow_redirects=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
constants.log(f"Uploaded {filename} to {self.api_url}/{folder} successfully: {r.status_code}.")
|
||||||
|
except Exception as e:
|
||||||
|
constants.log(f"Upload fail: {e}")
|
||||||
|
|
||||||
|
cdef load_bytes(self, str filename, str folder):
|
||||||
|
hardware_service = HardwareService()
|
||||||
|
cdef str hardware = hardware_service.get_hardware_info()
|
||||||
|
if self.token is None:
|
||||||
|
self.login()
|
||||||
|
url = f"{self.api_url}/resources/get/{folder}"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = json.dumps(
|
||||||
|
{
|
||||||
|
"password": self.credentials.password,
|
||||||
|
"hardware": hardware,
|
||||||
|
"fileName": filename
|
||||||
|
}, indent=4)
|
||||||
|
response = requests.post(url, data=payload, headers=headers, stream=True)
|
||||||
|
if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN:
|
||||||
|
self.login()
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
response = requests.post(url, data=payload, headers=headers, stream=True)
|
||||||
|
|
||||||
|
if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
|
||||||
|
print('500!')
|
||||||
|
|
||||||
|
hw_hash = Security.get_hw_hash(hardware)
|
||||||
|
key = Security.get_api_encryption_key(self.credentials, hw_hash)
|
||||||
|
|
||||||
|
resp_bytes = response.raw.read()
|
||||||
|
data = Security.decrypt_to(resp_bytes, key)
|
||||||
|
constants.log(<str>f'Downloaded file: {filename}, {len(data)} bytes')
|
||||||
|
return data
|
||||||
|
|
||||||
|
cdef load_big_small_resource(self, str resource_name, str folder, str key):
|
||||||
|
cdef str big_part = path.join(<str>folder, f'{resource_name}.big')
|
||||||
|
cdef str small_part = f'{resource_name}.small'
|
||||||
|
|
||||||
|
with open(<str>big_part, 'rb') as binary_file:
|
||||||
|
encrypted_bytes_big = binary_file.read()
|
||||||
|
|
||||||
|
encrypted_bytes_small = self.load_bytes(small_part, folder)
|
||||||
|
|
||||||
|
encrypted_bytes = encrypted_bytes_small + encrypted_bytes_big
|
||||||
|
result = Security.decrypt_to(encrypted_bytes, key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key):
|
||||||
|
cdef str big_part_name = f'{resource_name}.big'
|
||||||
|
cdef str small_part_name = f'{resource_name}.small'
|
||||||
|
|
||||||
|
resource_encrypted = Security.encrypt_to(<bytes>resource, key)
|
||||||
|
part_small_size = min(constants.SMALL_SIZE_KB * 1024, int(0.3 * len(resource_encrypted)))
|
||||||
|
part_small = resource_encrypted[:part_small_size] # slice bytes for part1
|
||||||
|
|
||||||
|
part_big = resource_encrypted[part_small_size:]
|
||||||
|
|
||||||
|
self.cdn_manager.upload(<str>constants.MODELS_FOLDER, <str>big_part_name, part_big)
|
||||||
|
with open(path.join(<str>folder, <str>big_part_name), 'wb') as f:
|
||||||
|
f.write(part_big)
|
||||||
|
self.upload_file(small_part_name, part_small, constants.MODELS_FOLDER)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_submodules
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
datas = [('venv\\Lib\\site-packages\\cv2', 'cv2')]
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'cdn_manager', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'tensorrt_engine', 'onnx_engine', 'inference_engine', 'inference', 'remote_command_handler']
|
||||||
|
hiddenimports += collect_submodules('cv2')
|
||||||
|
tmp_ret = collect_all('requests')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('psutil')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('msgpack')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('zmq')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('cryptography')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('numpy')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('onnxruntime')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('tensorrt')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('pycuda')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('pynvml')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('boto3')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('jwt')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['start.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='azaion-inference',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='azaion-inference',
|
||||||
|
)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
echo Build Cython app
|
||||||
|
|
||||||
|
cd %~dp0
|
||||||
|
|
||||||
|
echo remove dist folder:
|
||||||
|
if exist dist rmdir dist /s /q
|
||||||
|
if exist build rmdir build /s /q
|
||||||
|
|
||||||
|
echo install python and dependencies
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
venv\Scripts\python -m pip install --upgrade pip
|
||||||
|
venv\Scripts\pip install -r requirements.txt
|
||||||
|
venv\Scripts\pip install --upgrade pyinstaller pyinstaller-hooks-contrib
|
||||||
|
REM Testing autobuild in Jenkins!!!Latest check
|
||||||
|
|
||||||
|
venv\Scripts\python setup.py build_ext --inplace
|
||||||
|
|
||||||
|
echo install azaion-inference
|
||||||
|
venv\Scripts\pyinstaller --name=azaion-inference ^
|
||||||
|
--collect-submodules cv2 ^
|
||||||
|
--add-data "venv\Lib\site-packages\cv2;cv2" ^
|
||||||
|
--collect-all requests ^
|
||||||
|
--collect-all psutil ^
|
||||||
|
--collect-all msgpack ^
|
||||||
|
--collect-all zmq ^
|
||||||
|
--collect-all cryptography ^
|
||||||
|
--collect-all numpy ^
|
||||||
|
--collect-all onnxruntime ^
|
||||||
|
--collect-all tensorrt ^
|
||||||
|
--collect-all pycuda ^
|
||||||
|
--collect-all pynvml ^
|
||||||
|
--collect-all boto3 ^
|
||||||
|
--collect-all jwt ^
|
||||||
|
--hidden-import constants ^
|
||||||
|
--hidden-import annotation ^
|
||||||
|
--hidden-import credentials ^
|
||||||
|
--hidden-import file_data ^
|
||||||
|
--hidden-import user ^
|
||||||
|
--hidden-import security ^
|
||||||
|
--hidden-import secure_model ^
|
||||||
|
--hidden-import cdn_manager ^
|
||||||
|
--hidden-import api_client ^
|
||||||
|
--hidden-import hardware_service ^
|
||||||
|
--hidden-import remote_command ^
|
||||||
|
--hidden-import ai_config ^
|
||||||
|
--hidden-import tensorrt_engine ^
|
||||||
|
--hidden-import onnx_engine ^
|
||||||
|
--hidden-import inference_engine ^
|
||||||
|
--hidden-import inference ^
|
||||||
|
--hidden-import remote_command_handler ^
|
||||||
|
start.py
|
||||||
|
|
||||||
|
xcopy /E dist\azaion-inference ..\dist\
|
||||||
|
copy config.production.yaml ..\dist\config.yaml
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
cdef class CDNCredentials:
|
||||||
|
cdef str host
|
||||||
|
cdef str downloader_access_key
|
||||||
|
cdef str downloader_access_secret
|
||||||
|
cdef str uploader_access_key
|
||||||
|
cdef str uploader_access_secret
|
||||||
|
|
||||||
|
cdef class CDNManager:
|
||||||
|
cdef CDNCredentials creds
|
||||||
|
cdef object download_client
|
||||||
|
cdef object upload_client
|
||||||
|
|
||||||
|
cdef upload(self, str bucket, str filename, bytes file_bytes)
|
||||||
|
cdef download(self, str bucket, str filename)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import io
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
|
cdef class CDNCredentials:
|
||||||
|
def __init__(self, host, downloader_access_key, downloader_access_secret, uploader_access_key, uploader_access_secret):
|
||||||
|
self.host = host
|
||||||
|
self.downloader_access_key = downloader_access_key
|
||||||
|
self.downloader_access_secret = downloader_access_secret
|
||||||
|
self.uploader_access_key = uploader_access_key
|
||||||
|
self.uploader_access_secret = uploader_access_secret
|
||||||
|
|
||||||
|
|
||||||
|
cdef class CDNManager:
|
||||||
|
def __init__(self, CDNCredentials credentials):
|
||||||
|
|
||||||
|
self.creds = credentials
|
||||||
|
self.download_client = boto3.client('s3', endpoint_url=self.creds.host,
|
||||||
|
aws_access_key_id=self.creds.downloader_access_key,
|
||||||
|
aws_secret_access_key=self.creds.downloader_access_secret)
|
||||||
|
self.upload_client = boto3.client('s3', endpoint_url=self.creds.host,
|
||||||
|
aws_access_key_id=self.creds.uploader_access_key,
|
||||||
|
aws_secret_access_key=self.creds.uploader_access_secret)
|
||||||
|
|
||||||
|
cdef upload(self, str bucket, str filename, bytes file_bytes):
|
||||||
|
try:
|
||||||
|
self.upload_client.upload_fileobj(io.BytesIO(file_bytes), bucket, filename)
|
||||||
|
print(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}')
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
cdef download(self, str bucket, str filename):
|
||||||
|
try:
|
||||||
|
self.download_client.download_file(bucket, filename, filename)
|
||||||
|
print(f'downloaded {filename} from the {bucket} to current folder')
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
cdef str CONFIG_FILE # Port for the zmq
|
||||||
|
|
||||||
|
cdef int QUEUE_MAXSIZE # Maximum size of the command queue
|
||||||
|
cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit
|
||||||
|
cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit
|
||||||
|
|
||||||
|
cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api
|
||||||
|
|
||||||
|
cdef str AI_ONNX_MODEL_FILE
|
||||||
|
|
||||||
|
cdef str CDN_CONFIG
|
||||||
|
cdef str MODELS_FOLDER
|
||||||
|
|
||||||
|
cdef int SMALL_SIZE_KB
|
||||||
|
|
||||||
|
cdef bytes DONE_SIGNAL
|
||||||
|
|
||||||
|
|
||||||
|
cdef log(str log_message, bytes client_id=*)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
cdef str CONFIG_FILE = "config.yaml" # Port for the zmq
|
||||||
|
|
||||||
|
cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue
|
||||||
|
cdef str COMMANDS_QUEUE = "azaion-commands"
|
||||||
|
cdef str ANNOTATIONS_QUEUE = "azaion-annotations"
|
||||||
|
|
||||||
|
cdef str QUEUE_CONFIG_FILENAME = "secured-config.json"
|
||||||
|
|
||||||
|
cdef str AI_ONNX_MODEL_FILE = "azaion.onnx"
|
||||||
|
|
||||||
|
cdef str CDN_CONFIG = "cdn.yaml"
|
||||||
|
cdef str MODELS_FOLDER = "models"
|
||||||
|
|
||||||
|
cdef int SMALL_SIZE_KB = 3
|
||||||
|
|
||||||
|
cdef log(str log_message, bytes client_id=None):
|
||||||
|
local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
|
||||||
|
client_str = '' if client_id is None else f' {client_id}'
|
||||||
|
print(f'[{local_time}{client_str}]: {log_message}')
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
cdef class Credentials:
|
||||||
|
cdef public str email
|
||||||
|
cdef public str password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from msgpack import unpackb
|
||||||
|
|
||||||
|
cdef class Credentials:
|
||||||
|
|
||||||
|
def __init__(self, str email, str password):
|
||||||
|
self.email = email
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data):
|
||||||
|
unpacked = unpackb(data, strict_map_key=False)
|
||||||
|
return Credentials(
|
||||||
|
unpacked.get("Email"),
|
||||||
|
unpacked.get("Password"))
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
cdef class FileData:
|
||||||
|
cdef public str folder
|
||||||
|
cdef public str filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from msgpack import unpackb
|
||||||
|
|
||||||
|
cdef class FileData:
|
||||||
|
|
||||||
|
def __init__(self, str folder, str filename):
|
||||||
|
self.folder = folder
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef from_msgpack(bytes data):
|
||||||
|
unpacked = unpackb(data, strict_map_key=False)
|
||||||
|
return FileData(
|
||||||
|
unpacked.get("Folder"),
|
||||||
|
unpacked.get("Filename"))
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
cdef class HardwareService:
|
||||||
|
cdef bint is_windows
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef has_nvidia_gpu()
|
||||||
|
cdef str get_hardware_info(self)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import subprocess
|
||||||
|
import pynvml
|
||||||
|
|
||||||
|
|
||||||
|
cdef class HardwareService:
|
||||||
|
"""Handles hardware information retrieval and hash generation."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
res = subprocess.check_output("ver", shell=True).decode('utf-8')
|
||||||
|
if "Microsoft Windows" in res:
|
||||||
|
self.is_windows = True
|
||||||
|
else:
|
||||||
|
self.is_windows = False
|
||||||
|
except Exception:
|
||||||
|
print('Error during os type checking')
|
||||||
|
self.is_windows = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
cdef has_nvidia_gpu():
|
||||||
|
try:
|
||||||
|
pynvml.nvmlInit()
|
||||||
|
device_count = pynvml.nvmlDeviceGetCount()
|
||||||
|
|
||||||
|
if device_count > 0:
|
||||||
|
print(f"Found NVIDIA GPU(s).")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("No NVIDIA GPUs found by NVML.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except pynvml.NVMLError as error:
|
||||||
|
print(f"Failed to find NVIDIA GPU")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
pynvml.nvmlShutdown()
|
||||||
|
except:
|
||||||
|
print('Failed to shutdown pynvml cause probably no NVidia GPU')
|
||||||
|
pass
|
||||||
|
|
||||||
|
cdef str get_hardware_info(self):
|
||||||
|
if self.is_windows:
|
||||||
|
os_command = (
|
||||||
|
"powershell -Command \""
|
||||||
|
"Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; "
|
||||||
|
"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; "
|
||||||
|
"Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output; "
|
||||||
|
"(Get-Disk | Where-Object {$_.IsSystem -eq $true}).SerialNumber"
|
||||||
|
"\""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
os_command = (
|
||||||
|
"/bin/bash -c \" lscpu | grep 'Model name:' | cut -d':' -f2 && "
|
||||||
|
"lspci | grep VGA | cut -d':' -f3 && "
|
||||||
|
"free -g | grep Mem: | awk '{print $2}' && \""
|
||||||
|
"udevadm info --query=property --name=\"/dev/$(lsblk -no pkname \"$(findmnt -n -o SOURCE --target /)\")\" | grep -E 'ID_SERIAL=|ID_SERIAL_SHORT=' | cut -d'=' -f2- | head -n1 && "
|
||||||
|
)
|
||||||
|
# in case of subprocess error do:
|
||||||
|
# cdef bytes os_command_bytes = os_command.encode('utf-8')
|
||||||
|
# and use os_command_bytes
|
||||||
|
|
||||||
|
result = subprocess.check_output(os_command, shell=True).decode('utf-8')
|
||||||
|
lines = [line.strip() for line in result.splitlines() if line.strip()]
|
||||||
|
|
||||||
|
cdef str cpu = lines[0].replace("Name=", "").replace(" ", " ")
|
||||||
|
cdef str gpu = lines[1].replace("Name=", "").replace(" ", " ")
|
||||||
|
# could be multiple gpus
|
||||||
|
|
||||||
|
len_lines = len(lines)
|
||||||
|
cdef str memory = lines[len_lines-2].replace("TotalVisibleMemorySize=", "").replace(" ", " ")
|
||||||
|
cdef str drive_serial = lines[len_lines-1]
|
||||||
|
|
||||||
|
cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}'
|
||||||
|
return res
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user