136 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh e090f2d093 bump ImageSharp version vulnerability fix
decouple Loader from Common dll
fix current user url in api
2025-10-02 10:06:45 +03:00
Oleksandr Bezdieniezhnykh 51248edbd3 restrict input for numeric controls
allow input dots
2025-10-01 20:12:30 +03:00
Oleksandr Bezdieniezhnykh 5e226d422d fix textbox keyboard global handling
add CameraConfig to prod json
add launch and tasks jsons to vscode folder
2025-10-01 20:04:45 +03:00
Oleksandr Bezdieniezhnykh 99b9058187 fix saving scale for datasetexplorer and annotation 2025-10-01 14:29:23 +03:00
Oleksandr Bezdieniezhnykh 9b80eaf435 fix get installer url 2025-09-29 18:38:49 +03:00
Oleksandr Bezdieniezhnykh 429b39ee32 show filename for dataset explorer view 2025-09-29 17:52:00 +03:00
Oleksandr Bezdieniezhnykh 3a6ed60ea0 fix dataset explorer view 2025-09-29 17:37:34 +03:00
Oleksandr Bezdieniezhnykh 5500bda6ce refine module exclusions in azaion-loader-safe.spec to reduce false positives 2025-09-24 14:40:08 +03:00
Oleksandr Bezdieniezhnykh 9e25110016 address false positive vulnerability detection #1.1 2025-09-24 14:16:48 +03:00
Oleksandr Bezdieniezhnykh 7311f08884 address false positive vulnerability detection #1 2025-09-24 14:08:36 +03:00
Oleksandr Bezdieniezhnykh 2ee85d2e64 check installer correctly 2025-09-23 17:35:03 +03:00
Oleksandr Bezdieniezhnykh e501279b91 fix pyinstaller version 2025-09-23 14:36:10 +03:00
Oleksandr Bezdieniezhnykh 0549c2de7e Revert "do not use Loader to check creds"
This reverts commit 1d32c224ba.
2025-09-23 14:34:02 +03:00
Oleksandr Bezdieniezhnykh 1d32c224ba do not use Loader to check creds 2025-09-23 14:03:05 +03:00
Oleksandr Bezdieniezhnykh 2dc60a7ef4 fix all versions in requirements.txt 2025-09-23 13:21:51 +03:00
Oleksandr Bezdieniezhnykh fde9a9f418 add altitude + camera spec component and calc tile size by this
also restrict detections to be no bigger than in classes.json
2025-09-23 01:48:10 +03:00
Oleksandr Bezdieniezhnykh b0e4b467c1 add media removal 2025-09-06 01:27:49 +03:00
Oleksandr Bezdieniezhnykh 053719c4a8 fix inference status sending and logging 2025-09-06 00:44:08 +03:00
Oleksandr Bezdieniezhnykh 91ffa694f9 fix typo in robocopy command for inference file 2025-09-05 18:49:40 +03:00
Oleksandr Bezdieniezhnykh 2ba2b0aa8d enable process start in InferenceClient 2025-09-05 16:51:30 +03:00
Oleksandr Bezdieniezhnykh 9a16099194 fix split tile size
rework inference events and handling
todo: add Medias table and reflect recognition status there
2025-09-05 16:50:09 +03:00
Oleksandr Bezdieniezhnykh 7d68f7faee fix race condition issue
put AIAvailabilityEnum.ENABLED to the end of model conversion
2025-09-04 15:39:43 +03:00
Oleksandr Bezdieniezhnykh be77a81875 fix converting model initialization 2025-09-04 10:39:41 +03:00
Oleksandr Bezdieniezhnykh b3665630ed add warning ai status, fix ai availability handling 1 2025-09-03 16:36:26 +03:00
Oleksandr Bezdieniezhnykh a7a99c49c0 fix converting model 2025-09-03 14:10:46 +03:00
Denys Zaitsev 7bbdb61253 Merge remote-tracking branch 'origin/stage' into stage 2025-09-02 19:55:25 +03:00
Denys Zaitsev 9b93e39d37 added retry mechanism to upload.cmd and date/time logging for debug 2025-09-02 19:55:14 +03:00
Oleksandr Bezdieniezhnykh b2b2efe120 make version check more resilient to api installer availability 2025-09-02 19:32:40 +03:00
Oleksandr Bezdieniezhnykh d396677451 fix build for the new file 2025-09-02 17:01:15 +03:00
Oleksandr Bezdieniezhnykh 5ef81fab22 fix loader bug with _CACHED_HW_INFO
put tile size to name and set it dynamically for AI recognition
2025-09-02 14:00:47 +03:00
Oleksandr Bezdieniezhnykh b82de5f573 Merge remote-tracking branch 'origin/stage' into stage 2025-09-02 14:00:32 +03:00
Oleksandr Bezdieniezhnykh b6b6751c37 fix loader bug with _CACHED_HW_INFO
put tile size to name and set it dynamically for AI recognition
2025-09-02 13:59:23 +03:00
Denys Zaitsev fa3f31b147 Merge remote-tracking branch 'origin/stage' into stage 2025-09-02 12:29:39 +03:00
Denys Zaitsev c061260620 fix for google drive upload script to handle full installer as well 2025-09-02 12:29:16 +03:00
Oleksandr Bezdieniezhnykh 067f02cc63 update AI initializing
rework AIAvailabilityStatus events to mediatr
2025-09-01 20:12:13 +03:00
Oleksandr Bezdieniezhnykh d1ce9d9365 fix editing tiled images 2025-08-14 12:54:32 +03:00
Oleksandr Bezdieniezhnykh eb9e2a6f47 don't update loaderconfig.json on each update 2025-08-14 10:49:36 +03:00
Oleksandr Bezdieniezhnykh 55d8a5cb85 small improvements 2025-08-14 04:43:08 +03:00
Oleksandr Bezdieniezhnykh 61c93e9c88 clamp detections to media borders - create, move, resize
fix inference start
fix config
fix resize rectangles show
2025-08-14 04:22:55 +03:00
Oleksandr Bezdieniezhnykh 4780e8c61c fix detection label
fix schema migrator for enums
2025-08-13 10:12:25 +03:00
Oleksandr Bezdieniezhnykh 16e5853d67 put constant tile size temporarily 2025-08-12 14:58:21 +03:00
Oleksandr Bezdieniezhnykh 9e4dc5404c remove cpdef, add constants h 2025-08-12 14:53:14 +03:00
Oleksandr Bezdieniezhnykh ad782bcbaa splitting python complete 2025-08-12 14:48:56 +03:00
Oleksandr Bezdieniezhnykh fc6e5db795 add manual Tile Processor
zoom on video on pause (temp image)
2025-07-28 12:39:52 +03:00
Alex Bezdieniezhnykh fefd054ea0 fixed selection on editor
fixed image view and play
2025-07-11 22:46:25 +03:00
Alex Bezdieniezhnykh 938fd36aec fix zooming in map matcher 2025-07-08 19:33:22 +03:00
Alex Bezdieniezhnykh 6229ca8a03 rework autoupdate to script only
zoom fix
2025-07-06 23:22:21 +03:00
Alex Bezdieniezhnykh 75d3a2412f fix loader version check 2025-07-03 19:36:48 +03:00
Denys Zaitsev f6f0b0b266 fix for google drive upload script 2025-07-03 15:08:43 +03:00
Denys Zaitsev 291398a318 full installer fix inno script 2025-07-03 13:31:30 +03:00
Alex Bezdieniezhnykh 074c098d67 Merge branch 'dev' into stage 2025-07-03 13:09:38 +03:00
Alex Bezdieniezhnykh a68156a4d5 fix inno scripts 2025-07-03 13:09:11 +03:00
Alex Bezdieniezhnykh ea7c487493 fix show image, fix zoom 2025-07-03 12:53:43 +03:00
Denys Zaitsev eac6460645 added env vars for stage/prod and updated gdrive upload for production (separate folder). 2025-07-03 12:49:24 +03:00
Alex Bezdieniezhnykh bb6413c4f0 put default config if no config.json found
refactor Constants
2025-06-30 20:06:02 +03:00
Alex Bezdieniezhnykh 6eeb6d20bf fix build script 2025-06-30 16:43:06 +03:00
Alex Bezdieniezhnykh c07cac7b02 Merge remote-tracking branch 'origin/dev' into stage 2025-06-30 00:01:37 +03:00
Alex Bezdieniezhnykh c42faa1e2e add zoom and panning
zoom: ctrl + wheel, zoomed image:
ctrl + move mousr
2025-06-30 00:01:20 +03:00
Alex Bezdieniezhnykh 71f2967d4d Merge branch 'dev' into stage 2025-06-24 08:31:05 +03:00
Alex Bezdieniezhnykh a5c72c49f1 send port in image matcher arguments
lower expectations from gmaps, enlarge size of download map
2025-06-24 02:13:30 +03:00
Alex Bezdieniezhnykh f58dd3d04f switcher dataset explorer
lat lon -> geopoint
correct location for gps if small keypoints number
2025-06-24 02:13:30 +03:00
Alex Bezdieniezhnykh 627e63e543 send port in image matcher arguments
lower expectations from gmaps, enlarge size of download map
2025-06-24 02:12:52 +03:00
Alex Bezdieniezhnykh 253f811125 switcher dataset explorer
lat lon -> geopoint
correct location for gps if small keypoints number
2025-06-23 20:47:28 +03:00
Denys Zaitsev ad1e39268c Merge remote-tracking branch 'origin/stage' into stage 2025-06-17 12:53:17 +03:00
Denys Zaitsev 3277e8fa32 another fix for retention script 2025-06-17 12:53:01 +03:00
Alex Bezdieniezhnykh f1fb9e8ae9 fix flags in inno setup
fix build inference
2025-06-15 23:21:56 +03:00
Alex Bezdieniezhnykh d96f25e843 Merge branch 'dev' into stage 2025-06-15 23:04:22 +03:00
Alex Bezdieniezhnykh c5e72669c5 Create desktop icon: checked 2025-06-15 23:04:05 +03:00
Alex Bezdieniezhnykh 608bb94422 fix loaderconfig check 2025-06-15 21:50:43 +03:00
Alex Bezdieniezhnykh ca0e764284 Merge branch 'stage' 2025-06-15 21:32:09 +03:00
Alex Bezdieniezhnykh fd4ab90dc9 Merge branch 'dev' into stage 2025-06-15 21:31:39 +03:00
Alex Bezdieniezhnykh 9568e3fe26 fix numpy version 2025-06-15 21:30:37 +03:00
Denys Zaitsev fe5816bc9e fix for retention script 2025-06-15 19:57:05 +03:00
Denys Zaitsev 13659bee24 fix for retention script 2025-06-15 19:47:14 +03:00
Denys Zaitsev fd9506fcb3 fix for retention script 2025-06-15 19:45:28 +03:00
Alex Bezdieniezhnykh ec2f33b283 add loaderui versioning 2025-06-15 17:13:47 +03:00
Alex Bezdieniezhnykh ecace7683d add loaderconfig 2025-06-15 16:57:35 +03:00
Alex Bezdieniezhnykh 2e95ea8e94 fix message pack version 2025-06-15 15:31:18 +03:00
Alex Bezdieniezhnykh def7aad833 add resource check
incorrect pass / hw handling in a loader
2025-06-15 15:01:55 +03:00
Alex Bezdieniezhnykh c0f8dd792d fixed console Log
fix same files problem in python different libs
correct command logging in command handler
2025-06-14 21:01:32 +03:00
Alex Bezdieniezhnykh 09cfcdf61a create clients without window 2025-06-14 16:11:17 +03:00
Alex Bezdieniezhnykh 0d0176aac2 Merge branch 'refs/heads/dev'
# Conflicts:
#	Azaion.Inference/requirements.txt
2025-06-14 16:09:33 +03:00
Alex Bezdieniezhnykh 6f297c4ebf write logs for inference and loader to file 2025-06-14 16:08:32 +03:00
Alex Bezdieniezhnykh 8aa2f563a4 consolidate CommonSecurity to Common.dll 2025-06-13 23:06:48 +03:00
Alex Bezdieniezhnykh 904bc688ca fix inference bug in loading model 2025-06-11 07:23:14 +03:00
Alex Bezdieniezhnykh f9815a0a3f add list files for autoupdate feature
put new Versioning
fix bugs
2025-06-10 23:38:37 +03:00
Alex Bezdieniezhnykh dcd0fabc1f add loader and versioning 2025-06-10 08:53:57 +03:00
Alex Bezdieniezhnykh 7750025631 separate load functionality from inference client to loader client. Call loader client from inference to get the model.
remove dummy dlls, remove resource loader from c#.

TODO: Load dlls separately by Loader UI and loader client

WIP
2025-06-06 20:04:03 +03:00
Alex Bezdieniezhnykh 500db31142 add azaion loader 2025-06-01 19:16:49 +03:00
Alex Bezdieniezhnykh 2584b4f125 hardware service works on linux 2025-05-31 17:48:26 +03:00
Alex Bezdieniezhnykh 42867560c3 fix checking api client to download newest version 2025-05-30 17:18:55 +03:00
Alex Bezdieniezhnykh b5f2a75b86 bump up
version
2025-05-30 11:24:17 +03:00
Alex Bezdieniezhnykh 1b6c440dcc fix re-send new batch to gps denied
todo: clear folders, consider better center point to fetch next batch from satellite provider
2025-05-30 11:03:00 +03:00
Alex Bezdieniezhnykh b345137f16 fix bugs with UI for gps denied 2025-05-30 02:09:15 +03:00
Alex Bezdieniezhnykh d87ddb5f6a cleanup 2025-05-29 00:36:40 +03:00
Alex Bezdieniezhnykh d842466594 gps matcher async
put cryptography lib to fixed version
fix race condition bug in queue handler
add lock to db writing and backup to file db on each write
2025-05-29 00:35:35 +03:00
Alex Bezdieniezhnykh 34ea821fb3 better view for class distribution 2025-05-27 13:26:37 +03:00
dzaitsev e957f1192a updatedd zip_full.ps1 top pick only the latest file by LastWriteTime 2025-05-23 12:11:48 +03:00
dzaitsev d90b2750e0 Merge remote-tracking branch 'origin/dev' into dev 2025-05-22 10:15:59 +03:00
dzaitsev cea8083422 updated gdrive_retention.ps1 2025-05-22 10:15:40 +03:00
Alex Bezdieniezhnykh 757fb1e454 add more information to annotationbulkmessage 2025-05-22 00:33:13 +03:00
dzaitsev 0d51f3c373 updated gdrive_retention.ps1 2025-05-21 21:30:54 +03:00
dzaitsev 176a3ed0a6 updated gdrive_upload_full.ps1 2025-05-21 20:52:42 +03:00
dzaitsev a2e4157b9f updated gdrive_upload_iterative.ps1 2025-05-21 20:46:37 +03:00
dzaitsev 080d98389d Revert "fixed workdir path"
This reverts commit d35b97fdec.
2025-05-21 20:08:44 +03:00
dzaitsev d35b97fdec fixed workdir path 2025-05-21 19:34:56 +03:00
dzaitsev 541429aabd added retention script for gdrive 2025-05-21 18:42:55 +03:00
dzaitsev 39ed74996d updated zip and gdrive upload files. 2025-05-21 18:28:20 +03:00
dzaitsev f870198011 Merge remote-tracking branch 'origin/dev' into dev 2025-05-21 14:13:06 +03:00
dzaitsev 12aee103df Merge remote-tracking branch 'origin/pipeline' into dev 2025-05-21 14:08:40 +03:00
Alex Bezdieniezhnykh 522af51a8d don't send image to the queue on editing 2025-05-20 12:31:18 +03:00
Alex Bezdieniezhnykh edd803c304 fix classes colors, add caponier 2025-05-20 12:03:52 +03:00
Alex Bezdieniezhnykh a5fcb0988b fixed sorting in datasetexplorer, also show date
make annotationstatus more clear
2025-05-20 11:02:24 +03:00
Alex Bezdieniezhnykh 66bfe474c2 fix security 2 2025-05-18 20:31:43 +03:00
Alex Bezdieniezhnykh f2b57dccc0 security fix 2025-05-18 20:14:19 +03:00
Alex Bezdieniezhnykh c5e81ebcc6 fixed bugs with queue handling. At least most of them 2025-05-18 20:11:19 +03:00
Alex Bezdieniezhnykh cf563571c8 log queue errors 2025-05-17 19:38:07 +03:00
Alex Bezdieniezhnykh dae342b70e visual fixes 2025-05-17 19:34:02 +03:00
Alex Bezdieniezhnykh d02550f5a0 huge queue refactoring:
3 queues -> 1 queue
send delete validate updates
2025-05-17 19:25:33 +03:00
Alex Bezdieniezhnykh 87ceaa805b prepare to build 2025-05-14 19:53:35 +03:00
Alex Bezdieniezhnykh f41d49bd26 Merge remote-tracking branch 'origin/pipeline' into dev 2025-05-14 13:01:13 +03:00
Alex Bezdieniezhnykh 58cd0b06e9 Merge remote-tracking branch 'origin/dev' into dev 2025-05-14 12:45:03 +03:00
dzaitsev d92da6afa4 Errors sending to UI
notifying client of AI model conversion
2025-05-14 12:43:50 +03:00
dzaitsev 85f4d47318 Merge remote-tracking branch 'origin/dev' into dev 2025-05-07 17:32:29 +03:00
dzaitsev 006499c2ee fixed pushd / pulld commands in cmd 2025-05-07 17:31:16 +03:00
Alex Bezdieniezhnykh 568901edb6 Merge remote-tracking branch 'origin/dev' into dev 2025-05-07 17:14:37 +03:00
dzaitsev 8bf9d1d7e2 altered pushd / pulld commands in cmd 2025-05-07 16:47:26 +03:00
Alex Bezdieniezhnykh 79770dc7fa fix installer 2025-05-07 16:31:39 +03:00
dzaitsev 22f047bbe5 changed robocopy return code 2025-05-07 16:18:38 +03:00
dzaitsev 52c724eac6 changed robocopy return code 2025-05-07 16:15:02 +03:00
dzaitsev 8d9fe62c53 changed robocopy return code 2025-05-07 16:12:06 +03:00
dzaitsev 41b96e2ce5 Revert "init.cmd - fix for Jenkins incorrect handle of pushd/popd"
This reverts commit b8b4a33f9f.
2025-05-07 15:58:48 +03:00
dzaitsev b8b4a33f9f init.cmd - fix for Jenkins incorrect handle of pushd/popd 2025-05-07 15:12:52 +03:00
Alex Bezdieniezhnykh b937ed8051 Separate installers to Full and Iterative 2025-05-07 12:29:43 +03:00
Alex Bezdieniezhnykh f49c4e9d37 show created first 2025-05-05 09:50:49 +03:00
Deen 7ebe600adf Update requirements.txt
opencv added version explicitly
2025-04-30 12:01:38 +03:00
247 changed files with 7585 additions and 3846 deletions
+11 -2
View File
@@ -1,6 +1,9 @@
.idea
bin
obj
*.dll
*.exe
*.log
.vs
*.DotSettings*
*.user
@@ -11,6 +14,12 @@ venv
*.c
*.pyd
cython_debug*
dist-dlls
dist-azaion
Azaion*.exe
Azaion*.bin
azaion\.*\.big
_internal
dist
AzaionSuiteInstaller.exe
azaion\.*\.big
*.jpg
+33
View File
@@ -0,0 +1,33 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Azaion.Suite (with credentials)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
"args": ["credsManual", "-e", "test-admin@azaion.com", "-p", "Az@1on1000TestT-addminn11"],
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Launch Azaion.Suite",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
"args": [],
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Attach to Azaion.Suite",
"type": "coreclr",
"request": "attach",
"processName": "Azaion.Suite"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Azaion.Suite.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Azaion.Suite.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Azaion.Suite.sln"
],
"problemMatcher": "$msCompile"
}
]
}
+33 -7
View File
@@ -76,6 +76,7 @@
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
@@ -177,6 +178,7 @@
<ListView.ContextMenu>
<ContextMenu Name="LvFilesContextMenu">
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
<MenuItem Header="Видалити..." Click="DeleteMedia" Background="WhiteSmoke" />
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
@@ -190,10 +192,20 @@
</GridView>
</ListView.View>
</ListView>
<controls1:CameraConfigControl
x:Name="CameraConfigControl"
Grid.Column="0"
Grid.Row="4"
Camera="{Binding Camera, RelativeSource={RelativeSource AncestorType=Window}, Mode=OneWay}"
>
</controls1:CameraConfigControl>
<controls1:DetectionClasses
x:Name="LvClasses"
Grid.Column="0"
Grid.Row="4">
Grid.Row="5">
</controls1:DetectionClasses>
<GridSplitter
@@ -201,7 +213,7 @@
ResizeDirection="Columns"
Grid.Column="1"
Grid.Row="1"
Grid.RowSpan="4"
Grid.RowSpan="5"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@@ -210,7 +222,7 @@
<wpf:VideoView
Grid.Row="1"
Grid.Column="2"
Grid.RowSpan="4"
Grid.RowSpan="5"
x:Name="VideoView">
<controls1:CanvasEditor x:Name="Editor"
Background="#01000000"
@@ -223,7 +235,7 @@
ResizeDirection="Columns"
Grid.Column="3"
Grid.Row="1"
Grid.RowSpan="4"
Grid.RowSpan="5"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
@@ -233,7 +245,7 @@
<DataGrid x:Name="DgAnnotations"
Grid.Column="4"
Grid.Row="1"
Grid.RowSpan="4"
Grid.RowSpan="5"
Background="Black"
Foreground="White"
RowHeaderWidth="0"
@@ -315,7 +327,7 @@
<ColumnDefinition Width="28" /> <!-- 10 -->
<ColumnDefinition Width="28" /> <!-- 11 -->
<ColumnDefinition Width="28" /> <!-- 12 -->
<ColumnDefinition Width="0" /> <!-- 13 -->
<ColumnDefinition Width="28" /> <!-- 13 -->
<ColumnDefinition Width="*" /> <!-- 14-->
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
@@ -496,11 +508,12 @@
<Button
x:Name="AIDetectBtn"
IsEnabled="False"
Grid.Column="10"
Padding="2" Width="25"
Height="25"
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="AutoDetect">
Click="AIDetectBtn_OnClick">
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
@@ -602,6 +615,19 @@
</Image.Source>
</Image>
</Button>
<Button Grid.Column="13"
Padding="2"
Width="25"
Height="25"
ToolTip="Аналіз стану БПЛА. Клавіша: [K]" Background="Black" BorderBrush="Black"
Click="RunDroneMaintenance">
<Path Stretch="Fill" Fill="LightGray" Data="
M128,7.10542736e-15 C198.692448,7.10542736e-15 256,57.307552 256,128 C256,140.931179 254.082471,153.414494 250.516246,165.181113 L384,298.666667
C407.564149,322.230816 407.564149,360.435851 384,384 C360.435851,407.564149 322.230816,407.564149 298.666667,384 L165.181113,250.516246
C153.414494,254.082471 140.931179,256 128,256 C57.307552,256 7.10542736e-15,198.692448 7.10542736e-15,128 C7.10542736e-15,114.357909
2.13416363,101.214278 6.08683609,88.884763 L66.6347809,149.333333 L126.649,129.346 L129.329,126.666 L149.333333,66.7080586 L88.7145729,6.14152881
C101.0933,2.15385405 114.29512,7.10542736e-15 128,7.10542736e-15 Z" />
</Button>
<StatusBar Grid.Column="14"
Background="#252525"
Foreground="White">
+193 -243
View File
@@ -14,6 +14,7 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.Common.Services.Inference;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs;
@@ -28,8 +29,8 @@ namespace Azaion.Annotator;
public partial class Annotator
{
private readonly AppConfig _appConfig;
private readonly LibVLC _libVLC;
private readonly AppConfig? _appConfig;
private readonly LibVLC _libVlc;
private readonly MediaPlayer _mediaPlayer;
private readonly IMediator _mediator;
private readonly FormState _formState;
@@ -37,62 +38,68 @@ public partial class Annotator
private readonly IConfigUpdater _configUpdater;
private readonly HelpWindow _helpWindow;
private readonly ILogger<Annotator> _logger;
private readonly AnnotationService _annotationService;
private readonly IDbFactory _dbFactory;
private readonly IInferenceService _inferenceService;
private readonly IInferenceClient _inferenceClient;
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout;
private bool _gpsPanelVisible = false;
private bool _gpsPanelVisible;
public readonly CancellationTokenSource MainCancellationSource = new();
public CancellationTokenSource DetectionCancellationSource = new();
public bool FollowAI = false;
public bool IsInferenceNow = false;
private readonly CancellationTokenSource _mainCancellationSource = new();
public CancellationTokenSource DetCancelSource = new();
private bool _isInferenceNow;
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();
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public Dictionary<string, MediaFileInfo> MediaFilesDict = new();
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
public string MainTitle { get; set; }
public CameraConfig Camera => _appConfig?.CameraConfig ?? new CameraConfig();
public Annotator(
IConfigUpdater configUpdater,
IOptions<AppConfig> appConfig,
LibVLC libVLC,
LibVLC libVlc,
MediaPlayer mediaPlayer,
IMediator mediator,
FormState formState,
HelpWindow helpWindow,
ILogger<Annotator> logger,
AnnotationService annotationService,
IDbFactory dbFactory,
IInferenceService inferenceService,
IInferenceClient inferenceClient,
IGpsMatcherService gpsMatcherService)
{
InitializeComponent();
// Initialize configuration and services BEFORE InitializeComponent so bindings can see real values
_appConfig = appConfig.Value;
_configUpdater = configUpdater;
_libVLC = libVLC;
_libVlc = libVlc;
_mediaPlayer = mediaPlayer;
_mediator = mediator;
_formState = formState;
_helpWindow = helpWindow;
_logger = logger;
_annotationService = annotationService;
_dbFactory = dbFactory;
_inferenceService = inferenceService;
_gpsMatcherService = gpsMatcherService;
_inferenceClient = inferenceClient;
// Ensure bindings (e.g., Camera) resolve immediately
DataContext = this;
InitializeComponent();
MainTitle = $"Azaion Annotator {Constants.GetLocalVersion()}";
Title = MainTitle;
Loaded += OnLoaded;
Closed += OnFormClosed;
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
TbFolder.TextChanged += async (sender, args) =>
TbFolder.TextChanged += async (_, _) =>
{
if (!Path.Exists(TbFolder.Text))
return;
@@ -100,16 +107,21 @@ public partial class Annotator
{
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
await ReloadFiles();
await SaveUserSettings();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
};
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
MapMatcherComponent.Init(_appConfig, _gpsMatcherService);
MapMatcherComponent.Init(_appConfig, gpsMatcherService);
// When camera settings change, persist config
CameraConfigControl.CameraChanged += (_, _) =>
{
if (_appConfig != null)
_configUpdater.Save(_appConfig);
};
}
private void OnLoaded(object sender, RoutedEventArgs e)
@@ -119,16 +131,13 @@ public partial class Annotator
_suspendLayout = true;
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.UIConfig.LeftPanelWidth);
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.UIConfig.RightPanelWidth);
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.LeftPanelWidth ?? Constants.DEFAULT_LEFT_PANEL_WIDTH);
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig?.UIConfig.RightPanelWidth ?? Constants.DEFAULT_RIGHT_PANEL_WIDTH);
_suspendLayout = false;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
TbFolder.Text = _appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR;
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
LvClasses.Init(_appConfig?.AnnotationConfig.DetectionClasses ?? Constants.DefaultAnnotationClasses);
}
public void BlinkHelp(string helpText, int times = 2)
@@ -152,31 +161,16 @@ public partial class Annotator
VideoView.MediaPlayer = _mediaPlayer;
//On start playing media
_mediaPlayer.Playing += async (sender, args) =>
_mediaPlayer.Playing += (_, _) =>
{
if (_formState.CurrentMrl == _mediaPlayer.Media?.Mrl)
return; //already loaded all the info
_formState.CurrentMrl = _mediaPlayer.Media?.Mrl ?? "";
uint vw = 0, vh = 0;
_mediaPlayer.Size(0, ref vw, ref vh);
_formState.CurrentVideoSize = new Size(vw, vh);
_formState.CurrentMediaSize = new Size(vw, vh);
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
await Dispatcher.Invoke(async () => await ReloadAnnotations());
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
{
await Task.Delay(100); //wait to load the frame and set on pause
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
_mediaPlayer.SetPause(true);
}
};
LvFiles.MouseDoubleClick += async (_, _) =>
{
if (IsInferenceNow)
FollowAI = false;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
};
@@ -185,12 +179,12 @@ public partial class Annotator
var selectedClass = args.DetectionClass;
Editor.CurrentAnnClass = selectedClass;
_mediator.Publish(new AnnClassSelectedEvent(selectedClass));
};
};
_mediaPlayer.PositionChanged += (o, args) =>
_mediaPlayer.PositionChanged += (_, _) =>
ShowTimeAnnotations(TimeSpan.FromMilliseconds(_mediaPlayer.Time));
VideoSlider.ValueChanged += (value, newValue) =>
VideoSlider.ValueChanged += (_, newValue) =>
_mediaPlayer.Position = (float)(newValue / VideoSlider.Maximum);
VideoSlider.KeyDown += (sender, args) =>
@@ -199,77 +193,64 @@ public partial class Annotator
Volume.ValueChanged += (_, newValue) =>
_mediator.Publish(new VolumeChangedEvent((int)newValue));
SizeChanged += async (_, _) => await SaveUserSettings();
LocationChanged += async (_, _) => await SaveUserSettings();
StateChanged += async (_, _) => await SaveUserSettings();
DgAnnotations.MouseDoubleClick += (sender, args) =>
{
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
if (dgRow != null)
OpenAnnotationResult((AnnotationResult)dgRow!.Item);
if (ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) is DataGridRow dgRow)
OpenAnnotationResult((Annotation)dgRow.Item);
};
DgAnnotations.KeyUp += async (sender, args) =>
DgAnnotations.KeyUp += async (_, args) =>
{
switch (args.Key)
{
case Key.Up:
case Key.Down: //cursor is already moved by system behaviour
OpenAnnotationResult((AnnotationResult)DgAnnotations.SelectedItem);
OpenAnnotationResult((Annotation)DgAnnotations.SelectedItem);
break;
case Key.Delete:
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (result != MessageBoxResult.OK)
return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
var annotations = res.Select(x => x.Annotation).ToList();
var res = DgAnnotations.SelectedItems.Cast<Annotation>().ToList();
var annotationNames = res.Select(x => x.Name).ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
break;
}
};
Editor.Mediator = _mediator;
DgAnnotations.ItemsSource = _formState.AnnotationResults;
}
public void OpenAnnotationResult(AnnotationResult res)
private void OpenAnnotationResult(Annotation ann)
{
if (IsInferenceNow)
FollowAI = false;
_mediaPlayer.SetPause(true);
Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
if (!ann.IsSplit)
Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)ann.Time.TotalMilliseconds;
Dispatcher.Invoke(() =>
{
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(res.Annotation.Time);
Editor.ClearExpiredAnnotations(ann.Time);
});
ShowAnnotations(res.Annotation, showImage: true);
ShowAnnotation(ann, showImage: true, openResult: true);
}
private Task SaveUserSettings()
private void SaveUserSettings()
{
if (_suspendLayout)
return Task.CompletedTask;
if (_suspendLayout || _appConfig is null)
return;
_appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
_appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
ThrottleExt.Throttle(() =>
{
_configUpdater.Save(_appConfig);
return Task.CompletedTask;
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
_configUpdater.Save(_appConfig);
}
private void ShowTimeAnnotations(TimeSpan time)
public void ShowTimeAnnotations(TimeSpan time, bool showImage = false)
{
Dispatcher.Invoke(() =>
{
@@ -277,49 +258,57 @@ public partial class Annotator
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(time);
});
ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault());
var annotations = TimedAnnotations.Query(time).ToList();
if (!annotations.Any())
return;
foreach (var ann in annotations)
ShowAnnotation(ann, showImage);
}
private void ShowAnnotations(Annotation? annotation, bool showImage = false)
private void ShowAnnotation(Annotation annotation, bool showImage = false, bool openResult = false)
{
if (annotation == null)
return;
Dispatcher.Invoke(async () =>
{
var videoSize = _formState.CurrentVideoSize;
if (showImage)
if (showImage && !annotation.IsSplit && File.Exists(annotation.ImagePath))
{
if (File.Exists(annotation.ImagePath))
{
Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
_formState.BackgroundTime = annotation.Time;
videoSize = Editor.RenderSize;
}
Editor.SetBackground(await annotation.ImagePath.OpenImage());
_formState.BackgroundTime = annotation.Time;
}
Editor.CreateDetections(annotation.Time, annotation.Detections, _appConfig.AnnotationConfig.DetectionClasses, videoSize);
if (annotation.SplitTile != null && openResult)
{
var canvasTileLocation = new CanvasLabel(new YoloLabel(annotation.SplitTile, _formState.CurrentMediaSize),
RenderSize);
Editor.ZoomTo(new Point(canvasTileLocation.CenterX, canvasTileLocation.CenterY));
}
else
Editor.CreateDetections(annotation, _appConfig?.AnnotationConfig.DetectionClasses ?? [], _formState.CurrentMediaSize);
});
}
private async Task ReloadAnnotations()
public async Task ReloadAnnotations()
{
_formState.AnnotationResults.Clear();
TimedAnnotations.Clear();
Editor.RemoveAllAnns();
var annotations = await _dbFactory.Run(async db =>
await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.OriginalMediaName == _formState.VideoName)
.OrderBy(x => x.Time)
.ToListAsync(token: MainCancellationSource.Token));
TimedAnnotations.Clear();
_formState.AnnotationResults.Clear();
foreach (var ann in annotations)
await Dispatcher.InvokeAsync(async () =>
{
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
_formState.AnnotationResults.Add(new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, ann));
}
_formState.AnnotationResults.Clear();
TimedAnnotations.Clear();
Editor.RemoveAllAnns();
var annotations = await _dbFactory.Run(async db =>
await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.OriginalMediaName == _formState.MediaName)
.OrderBy(x => x.Time)
.ToListAsync(token: _mainCancellationSource.Token));
TimedAnnotations.Clear();
_formState.AnnotationResults.Clear();
foreach (var ann in annotations)
{
// Duplicate for speed
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
_formState.AnnotationResults.Add(ann);
}
});
}
//Add manually
@@ -330,7 +319,7 @@ public partial class Annotator
TimedAnnotations.Remove(previousAnnotations);
TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time);
if (existingResult != null)
{
try
@@ -342,32 +331,29 @@ public partial class Annotator
_logger.LogError(e, e.Message);
throw;
}
}
var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Annotation.Time, Index = i })
.Select((x, i) => new { x.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index);
var index = dict.Where(x => x.Key < time)
.OrderBy(x => time - x.Key)
.Select(x => x.Value + 1)
.FirstOrDefault();
var annRes = new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation);
_formState.AnnotationResults.Insert(index, annRes);
_formState.AnnotationResults.Insert(index, annotation);
}
private async Task ReloadFiles()
{
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
var dir = new DirectoryInfo(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR);
if (!dir.Exists)
return;
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
var videoFiles = dir.GetFiles((_appConfig?.AnnotationConfig.VideoFormats ?? Constants.DefaultVideoFormats)
.ToArray()).Select(x =>
{
using var media = new Media(_libVLC, x.FullName);
var media = new Media(_libVlc, x.FullName);
media.Parse();
var fInfo = new MediaFileInfo
{
@@ -379,7 +365,7 @@ public partial class Annotator
return fInfo;
}).ToList();
var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
var imageFiles = dir.GetFiles((_appConfig?.AnnotationConfig.ImageFormats ?? Constants.DefaultImageFormats).ToArray())
.Select(x => new MediaFileInfo
{
Name = x.Name,
@@ -390,32 +376,32 @@ public partial class Annotator
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))
var labelsDict = await _dbFactory.Run(async db =>
await db.Annotations
.GroupBy(x => x.OriginalMediaName)
.Where(x => allFileNames.Contains(x.Key))
.ToDictionaryAsync(x => x.Key, x => x.Key));
.Select(x => x.Key)
.ToDictionaryAsync(x => x, x => x));
foreach (var mediaFile in allFiles)
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
.ToDictionary(gr => gr.Key, gr => gr.First());
LvFiles.ItemsSource = AllMediaFiles;
BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
DataContext = this;
}
private void OnFormClosed(object? sender, EventArgs e)
{
MainCancellationSource.Cancel();
_mainCancellationSource.Cancel();
_inferenceService.StopInference();
DetectionCancellationSource.Cancel();
DetCancelSource.Cancel();
_mediaPlayer.Stop();
_mediaPlayer.Dispose();
_libVLC.Dispose();
_libVlc.Dispose();
}
private void OpenContainingFolder(object sender, RoutedEventArgs e)
@@ -436,41 +422,38 @@ public partial class Annotator
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
}
private void SeekTo(TimeSpan time) =>
SeekTo((long)time.TotalMilliseconds);
private void OpenFolderItemClick(object sender, RoutedEventArgs e) => OpenFolder();
private void OpenFolderButtonClick(object sender, RoutedEventArgs e) => OpenFolder();
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async Task OpenFolder()
private void OpenFolder()
{
var dlg = new CommonOpenFileDialog
{
Title = "Open Video folder",
IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
InitialDirectory = Path.GetDirectoryName(_appConfig?.DirectoriesConfig.VideosDirectory ?? Constants.DEFAULT_VIDEO_DIR)
};
var dialogResult = dlg.ShowDialog();
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
return;
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
if (_appConfig is not null)
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
TbFolder.Text = dlg.FileName;
await Task.CompletedTask;
}
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
{
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.FName);
LvFiles.ItemsSource = FilteredMediaFiles;
LvFiles.ItemsSource = FilteredMediaFiles;
}
private void PlayClick(object sender, RoutedEventArgs e)
{
if (IsInferenceNow)
FollowAI = false;
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
}
@@ -493,7 +476,7 @@ public partial class Annotator
_helpWindow.Activate();
}
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings();
private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings();
private void LvFilesContextOpening(object sender, ContextMenuEventArgs e)
{
@@ -501,112 +484,48 @@ public partial class Annotator
LvFilesContextMenu.DataContext = listItem!.DataContext;
}
public void AutoDetect(object sender, RoutedEventArgs e)
private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e)
{
if (IsInferenceNow)
try
{
FollowAI = true;
return;
await AutoDetect();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
public async Task AutoDetect()
{
if (_isInferenceNow)
return;
if (LvFiles.Items.IsEmpty)
return;
if (LvFiles.SelectedIndex == -1)
LvFiles.SelectedIndex = 0;
Dispatcher.Invoke(() => Editor.ResetBackground());
Dispatcher.Invoke(() => Editor.SetBackground(null));
IsInferenceNow = true;
FollowAI = true;
DetectionCancellationSource = new CancellationTokenSource();
var detectToken = DetectionCancellationSource.Token;
_ = Task.Run(async () =>
{
while (!detectToken.IsCancellationRequested)
{
var files = new List<string>();
await Dispatcher.Invoke(async () =>
{
//Take all medias
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
//.Where(x => !x.HasAnnotations)
.Take(Constants.DETECTION_BATCH_SIZE)
.Select(x => x.Path)
.ToList() ?? [];
if (files.Count != 0)
{
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
await ReloadAnnotations();
}
});
if (files.Count == 0)
break;
_isInferenceNow = true;
AIDetectBtn.IsEnabled = false;
await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken);
DetCancelSource = new CancellationTokenSource();
Dispatcher.Invoke(() =>
{
if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
DetectionCancellationSource.Cancel();
LvFiles.SelectedIndex += files.Count;
});
}
Dispatcher.Invoke(() =>
{
LvFiles.Items.Refresh();
IsInferenceNow = false;
FollowAI = false;
});
});
}
var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles)
.Skip(LvFiles.SelectedIndex)
.Select(x => x.Path)
.ToList();
if (files.Count == 0)
return;
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
{
await Dispatcher.Invoke(async () =>
{
try
{
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
{
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
.Select((info, i) => new
{
MediaInfo = info,
Index = i
})
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
if (nextFile != null)
{
LvFiles.SelectedIndex = nextFile.Index;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
}
}
await _inferenceService.RunInference(files, _appConfig?.CameraConfig ?? Constants.DefaultCameraConfig, DetCancelSource.Token);
AddAnnotation(annotation);
if (FollowAI)
SeekTo(annotationImage.Milliseconds, false);
var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
Dispatcher.Invoke(() =>
{
if (_formState.CurrentMedia != null)
_formState.CurrentMedia.HasAnnotations = true;
LvFiles.Items.Refresh();
StatusHelp.Text = log;
});
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
LvFiles.Items.Refresh();
_isInferenceNow = false;
StatusHelp.Text = "Розпізнавання завершено";
AIDetectBtn.IsEnabled = true;
}
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
@@ -631,9 +550,40 @@ public partial class Annotator
}
}
#region Denys Wishes
private void SoundDetections(object sender, RoutedEventArgs e)
{
throw new NotImplementedException();
MessageBox.Show("Функція Аудіоаналіз знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void RunDroneMaintenance(object sender, RoutedEventArgs e)
{
MessageBox.Show("Функція Аналіз стану БПЛА знаходиться в стадії розробки","Система", MessageBoxButton.OK, MessageBoxImage.Information);
}
#endregion
private void DeleteMedia(object sender, RoutedEventArgs e)
{
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo;
if (mediaFileInfo == null)
return;
DeleteMedia(mediaFileInfo);
}
public void DeleteMedia(MediaFileInfo mediaFileInfo)
{
var obj = mediaFileInfo.MediaType == MediaTypes.Image
? "цю картинку "
: "це відео ";
var result = MessageBox.Show($"Видалити {obj}?",
"Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result != MessageBoxResult.Yes)
return;
File.Delete(mediaFileInfo.Path);
AllMediaFiles.Remove(mediaFileInfo);
}
}
@@ -641,7 +591,7 @@ public class GradientStyleSelector : StyleSelector
{
public override Style? SelectStyle(object item, DependencyObject container)
{
if (container is not DataGridRow row || row.DataContext is not AnnotationResult result)
if (container is not DataGridRow row || row.DataContext is not Annotation result)
return null;
var style = new Style(typeof(DataGridRow));
@@ -671,7 +621,7 @@ public class GradientStyleSelector : StyleSelector
foreach (var gradientStop in gradients)
brush.GradientStops.Add(gradientStop);
style.Setters.Add(new Setter(DataGridRow.BackgroundProperty, brush));
style.Setters.Add(new Setter(Control.BackgroundProperty, brush));
return style;
}
}
+282 -87
View File
@@ -1,13 +1,21 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.Controls;
using Azaion.Annotator.DTO;
using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO;
using Azaion.Common.Services.Inference;
using GMap.NET;
using GMap.NET.WindowsPresentation;
using LibVLCSharp.Shared;
using MediatR;
using Microsoft.Extensions.Logging;
@@ -17,25 +25,33 @@ using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator;
public class AnnotatorEventHandler(
LibVLC libVLC,
LibVLC libVlc,
MediaPlayer mediaPlayer,
Annotator mainWindow,
FormState formState,
AnnotationService annotationService,
IAnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig,
IInferenceService inferenceService)
IOptions<AnnotationConfig> annotationConfig,
IInferenceService inferenceService,
IDbFactory dbFactory,
IAzaionApi api,
FailsafeAnnotationsProducer producer)
:
INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent>
INotificationHandler<AnnotationsDeletedEvent>,
INotificationHandler<AnnotationAddedEvent>,
INotificationHandler<SetStatusTextEvent>,
INotificationHandler<GPSMatcherResultProcessedEvent>,
INotificationHandler<AIAvailabilityStatusEvent>
{
private const int STEP = 20;
private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280;
private readonly string _tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{
{ Key.Space, PlaybackControlEnum.Pause },
@@ -48,7 +64,7 @@ public class AnnotatorEventHandler(
{ Key.PageDown, PlaybackControlEnum.Next },
};
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken)
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct)
{
SelectClass(notification.DetectionClass);
await Task.CompletedTask;
@@ -62,7 +78,7 @@ public class AnnotatorEventHandler(
mainWindow.LvClasses.SelectNum(detClass.Id);
}
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default)
public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default)
{
if (keyEvent.WindowEnum != WindowEnum.Annotator)
return;
@@ -78,19 +94,19 @@ public class AnnotatorEventHandler(
SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, cancellationToken);
await ControlPlayback(value, ct);
if (key == Key.R)
mainWindow.AutoDetect(null!, null!);
await mainWindow.AutoDetect();
#region Volume
switch (key)
{
case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken);
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
break;
case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken);
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
break;
case Key.Up:
case Key.VolumeUp:
@@ -108,9 +124,9 @@ public class AnnotatorEventHandler(
#endregion
}
public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default)
public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default)
{
await ControlPlayback(notification.PlaybackControl, cancellationToken);
await ControlPlayback(notification.PlaybackControl, ct);
mainWindow.VideoView.Focus();
}
@@ -127,21 +143,26 @@ public class AnnotatorEventHandler(
await Play(cancellationToken);
break;
case PlaybackControlEnum.Pause:
mediaPlayer.Pause();
if (mainWindow.IsInferenceNow)
mainWindow.FollowAI = false;
if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue)
if (mediaPlayer.IsPlaying)
{
mainWindow.Editor.ResetBackground();
formState.BackgroundTime = null;
mediaPlayer.Pause();
mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
}
else
{
mediaPlayer.Play();
if (formState.BackgroundTime.HasValue)
{
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
}
}
break;
case PlaybackControlEnum.Stop:
inferenceService.StopInference();
await mainWindow.DetectionCancellationSource.CancelAsync();
await mainWindow.DetCancelSource.CancelAsync();
mediaPlayer.Stop();
break;
case PlaybackControlEnum.PreviousFrame:
@@ -151,11 +172,18 @@ public class AnnotatorEventHandler(
mainWindow.SeekTo(mediaPlayer.Time + step);
break;
case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(cancellationToken);
await SaveAnnotation(cancellationToken);
break;
case PlaybackControlEnum.RemoveSelectedAnns:
mainWindow.Editor.RemoveSelectedAnns();
var focusedElement = FocusManager.GetFocusedElement(mainWindow);
if (focusedElement is ListViewItem item)
{
if (item.DataContext is not MediaFileInfo mediaFileInfo)
return;
mainWindow.DeleteMedia(mediaFileInfo);
}
else
mainWindow.Editor.RemoveSelectedAnns();
break;
case PlaybackControlEnum.RemoveAllAnns:
mainWindow.Editor.RemoveAllAnns();
@@ -201,7 +229,7 @@ public class AnnotatorEventHandler(
await Play(ct);
}
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
{
ChangeVolume(notification.Volume);
await Task.CompletedTask;
@@ -218,87 +246,254 @@ public class AnnotatorEventHandler(
if (mainWindow.LvFiles.SelectedItem == null)
return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
mainWindow.Editor.ResetBackground();
if (formState.CurrentMedia == mediaInfo)
return; //already loaded
formState.CurrentMedia = mediaInfo;
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
if (formState.CurrentMedia.MediaType == MediaTypes.Image)
mediaPlayer.SetPause(true);
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
await mainWindow.ReloadAnnotations();
if (mediaInfo.MediaType == MediaTypes.Video)
{
mainWindow.Editor.SetBackground(null);
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop();
mediaPlayer.Play(new Media(libVlc, mediaInfo.Path));
}
else
{
formState.BackgroundTime = TimeSpan.Zero;
var image = await mediaInfo.Path.OpenImage();
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
mainWindow.Editor.SetBackground(image);
mediaPlayer.Stop();
mainWindow.ShowTimeAnnotations(TimeSpan.Zero, showImage: true);
}
}
//SAVE: MANUAL
private async Task SaveAnnotations(CancellationToken cancellationToken = default)
private async Task SaveAnnotation(CancellationToken cancellationToken = default)
{
if (formState.CurrentMedia == null)
return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var originalMediaName = formState.VideoName;
var fName = originalMediaName.ToTimeName(time);
var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList();
formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
var timeName = formState.MediaName.ToTimeName(time);
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{Constants.JPG_EXT}");
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
if (formState.BackgroundTime.HasValue)
formState.CurrentMedia.HasAnnotations = mainWindow.Editor.CurrentDetections.Count != 0;
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
if (isVideo)
{
//no need to save image, it's already there, just remove background
mainWindow.Editor.ResetBackground();
foreach (var annotation in annotations)
mainWindow.AddAnnotation(annotation);
mediaPlayer.Play();
// next item. Probably not needed
// var annGrid = mainWindow.DgAnnotations;
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
if (isVideo)
mediaPlayer.Play();
else
await NextMedia(ct: cancellationToken);
await NextMedia(ct: cancellationToken);
}
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
if (isVideo)
mainWindow.AddAnnotation(annotation);
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken cancellationToken = default)
{
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x);
foreach (var ann in notification.Annotations)
{
if (!annResDict.TryGetValue(ann.Name, out var value))
continue;
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
formState.AnnotationResults.Remove(value);
mainWindow.TimedAnnotations.Remove(ann);
}
if (formState.AnnotationResults.Count == 0)
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
var mediaSize = new Size(source.PixelWidth, source.PixelHeight);
var annotationsResult = new List<Annotation>();
if (!File.Exists(imgPath))
{
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
if (media != null)
if (mediaSize.FitSizeForAI())
await source.SaveImage(imgPath, cancellationToken);
else
{
media.HasAnnotations = false;
mainWindow.LvFiles.Items.Refresh();
//Tiling
//1. Convert from RenderSize to CurrentMediaSize
var detectionCoords = canvasDetections.Select(x => new CanvasLabel(
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
.ToList();
//2. Split to frames
var results = TileProcessor.Split(formState.CurrentMediaSize, detectionCoords, cancellationToken);
//3. Save each frame as a separate annotation
foreach (var res in results)
{
var time = TimeSpan.Zero;
var annotationName = $"{formState.MediaName}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
await bitmap.SaveImage(tileImgPath, cancellationToken);
var frameSize = new Size(res.Tile.Width, res.Tile.Height);
var detections = res.Detections
.Select(det => det.ReframeToSmall(res.Tile))
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
.ToList();
annotationsResult.Add(await annotationService.SaveAnnotation(formState.MediaName, annotationName, time, detections, token: cancellationToken));
}
return annotationsResult;
}
}
await Task.CompletedTask;
var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var annName = formState.MediaName.ToTimeName(timeImg);
var currentDetections = canvasDetections.Select(x =>
new Detection(annName, new YoloLabel(x, mainWindow.Editor.RenderSize, mediaSize)))
.ToList();
var annotation = await annotationService.SaveAnnotation(formState.MediaName, annName, timeImg, currentDetections, token: cancellationToken);
return [annotation];
}
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
{
try
{
mainWindow.Dispatcher.Invoke(() =>
{
var namesSet = notification.AnnotationNames.ToHashSet();
var remainAnnotations = formState.AnnotationResults
.Where(x => !namesSet.Contains(x.Name)).ToList();
formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann);
var timedAnnotationsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnotationsToRemove);
if (formState.AnnotationResults.Count == 0)
{
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
if (media != null)
{
media.HasAnnotations = false;
mainWindow.LvFiles.Items.Refresh();
}
}
});
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
foreach (var name in notification.AnnotationNames)
{
try
{
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
}
catch (Exception e)
{
logger.LogError(e, e.Message);
}
}
//Only validators can send Delete to the queue
if (!notification.FromQueue && api.CurrentUser.Role.IsValidator())
await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
{
mainWindow.Dispatcher.Invoke(() =>
{
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") == e.Annotation.OriginalMediaName)
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
mainWindow.LvFiles.Items.Refresh();
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
if (media != null)
media.HasAnnotations = true;
mainWindow.LvFiles.Items.Refresh();
mainWindow.StatusHelp.Text = log;
});
return Task.CompletedTask;
}
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.StatusHelp.Text = e.Text;
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
});
return Task.CompletedTask;
}
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
if (e.ProcessedGeoPoint != e.GeoPoint)
AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet);
ann.Lat = e.GeoPoint.Lat;
ann.Lon = e.GeoPoint.Lon;
});
return Task.CompletedTask;
}
private void AddMarker(GeoPoint point, string text, SolidColorBrush color)
{
var map = mainWindow.MapMatcherComponent;
var pointLatLon = new PointLatLng(point.Lat, point.Lon);
var marker = new GMapMarker(pointLatLon);
marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color);
map.SatelliteMap.Markers.Add(marker);
map.SatelliteMap.Position = pointLatLon;
map.SatelliteMap.ZoomAndCenterMarkers(null);
}
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
logger.LogInformation(e.ToString());
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
mainWindow.StatusHelp.Text = e.ToString();
});
if (e.Status is AIAvailabilityEnum.Enabled or AIAvailabilityEnum.Error)
await inferenceService.CheckAIAvailabilityTokenSource.CancelAsync();
}
}
+14 -11
View File
@@ -7,24 +7,27 @@
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
<PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RangeTree" Version="3.0.1" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
+3 -3
View File
@@ -11,7 +11,7 @@ namespace Azaion.Annotator.Controls
{
public readonly GMapMarker Marker;
public CircleVisual(GMapMarker m, Brush background)
public CircleVisual(GMapMarker m, int size, string text, Brush background)
{
ShadowEffect = new DropShadowEffect();
Marker = m;
@@ -22,14 +22,14 @@ namespace Azaion.Annotator.Controls
MouseLeave += CircleVisual_MouseLeave;
Loaded += OnLoaded;
Text = "?";
Text = text;
StrokeArrow.EndLineCap = PenLineCap.Triangle;
StrokeArrow.LineJoin = PenLineJoin.Round;
RenderTransform = _scale;
Width = Height = 22;
Width = Height = size;
FontSize = Width / 1.55;
Background = background;
+7 -7
View File
@@ -137,13 +137,13 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<controls:CanvasEditor
Grid.Column="2"
x:Name="GpsImageEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" >
</controls:CanvasEditor>
<Border Grid.Column="2" ClipToBounds="True">
<controls:CanvasEditor
x:Name="GpsImageEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
</Border>
<GridSplitter
Background="DarkGray"
ResizeDirection="Columns"
+7 -49
View File
@@ -1,12 +1,9 @@
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;
@@ -14,7 +11,6 @@ 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;
@@ -23,7 +19,7 @@ public partial class MapMatcher : UserControl
{
private AppConfig _appConfig = null!;
List<MediaFileInfo> _allMediaFiles = new();
private Dictionary<int, Annotation> _annotations = new();
public Dictionary<int, Annotation> Annotations = new();
private string _currentDir = null!;
private IGpsMatcherService _gpsMatcherService = null!;
@@ -47,7 +43,7 @@ public partial class MapMatcher : UserControl
private async Task OpenGpsLocation(int gpsFilesIndex)
{
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
var ann = _annotations.GetValueOrDefault(gpsFilesIndex);
var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
if (ann == null)
return;
@@ -101,51 +97,14 @@ public partial class MapMatcher : UserControl
_allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
_annotations = mediaFiles.Select((x, i) => (i, new Annotation
Annotations = mediaFiles.Select((x, i) => (i, new Annotation
{
Name = x.Name,
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);
}
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon);
}
private async void TestGps(object sender, RoutedEventArgs e)
@@ -153,8 +112,7 @@ public partial class MapMatcher : UserControl
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));
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon);
}
}
@@ -1,8 +0,0 @@
using System.Drawing;
namespace Azaion.Annotator.Extensions;
public static class RectangleFExtensions
{
public static double Area(this RectangleF rectangle) => rectangle.Width * rectangle.Height;
}
@@ -1,14 +0,0 @@
using System.ComponentModel;
namespace Azaion.Annotator;
public static class SynchronizeInvokeExtensions
{
public static void InvokeEx<T>(this T t, Action<T> action) where T : ISynchronizeInvoke
{
if (t.InvokeRequired)
t.Invoke(action, [t]);
else
action(t);
}
}
+11 -10
View File
@@ -4,28 +4,29 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="LazyCache" Version="2.4.0" />
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="MessagePack" Version="3.1.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.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
<PackageReference Include="NetMQ" Version="4.0.1.16" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
</ItemGroup>
</Project>
+203 -56
View File
@@ -1,111 +1,186 @@
using System.Windows;
using System.Windows.Media;
using Azaion.Common.Database;
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Newtonsoft.Json;
using Serilog;
using System.Windows;
namespace Azaion.Common;
public class Constants
public static class Constants
{
public const string JPG_EXT = ".jpg";
public const string CONFIG_PATH = "config.json";
public const string DEFAULT_API_URL = "https://api.azaion.com";
public const string AZAION_SUITE_EXE = "Azaion.Suite.exe";
public const int AI_TILE_SIZE_DEFAULT = 1280;
#region ExternalClientsConfig
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
private static readonly LoaderClientConfig DefaultLoaderClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
ApiUrl = DEFAULT_API_URL
};
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
private static readonly InferenceClientConfig DefaultInferenceClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
ApiUrl = DEFAULT_API_URL
};
private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255;
private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256;
private static readonly GpsDeniedClientConfig DefaultGpsDeniedClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT
};
#endregion ExternalClientsConfig
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string JPG_EXT = ".jpg";
public const string TXT_EXT = ".txt";
#region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels";
public const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
private const string DEFAULT_LABELS_DIR = "labels";
private const string DEFAULT_IMAGES_DIR = "images";
private const string DEFAULT_RESULTS_DIR = "results";
private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
#endregion
#region AnnotatorConfig
public static readonly AnnotationConfig DefaultAnnotationConfig = new()
public static readonly List<DetectionClass> DefaultAnnotationClasses =
[
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 },
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 },
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 },
new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 },
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 },
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 },
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 },
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 },
new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 },
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 },
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 },
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor(), MaxSizeM = 3 },
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor(), MaxSizeM = 14 },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor(), MaxSizeM = 8 },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor(), MaxSizeM = 15 },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor(), MaxSizeM = 20 },
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor(), MaxSizeM = 10 },
new() { Id = 17, Name = "Ammo", ShortName = "БК", Color = "#33658a".ToColor(), MaxSizeM = 2 },
new() { Id = 18, Name = "Protect.Struct", ShortName = "Зуби.драк", Color = "#969647".ToColor(), MaxSizeM = 2 }
];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi", "ts"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
private static readonly AnnotationConfig DefaultAnnotationConfig = new()
{
DetectionClasses = DefaultAnnotationClasses!,
VideoFormats = DefaultVideoFormats!,
ImageFormats = DefaultImageFormats!,
DetectionClasses = DefaultAnnotationClasses,
VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
private static readonly List<DetectionClass> DefaultAnnotationClasses =
[
new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() },
new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }
];
#region UIConfig
public const int DEFAULT_LEFT_PANEL_WIDTH = 200;
public const int DEFAULT_RIGHT_PANEL_WIDTH = 200;
#endregion UIConfig
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
#region CameraConfig
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
public const int DEFAULT_ALTITUDE = 400;
public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m;
public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m;
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
public static readonly CameraConfig DefaultCameraConfig = new()
{
Altitude = DEFAULT_ALTITUDE,
CameraFocalLength = DEFAULT_CAMERA_FOCAL_LENGTH,
CameraSensorWidth = DEFAULT_CAMERA_SENSOR_WIDTH
};
#endregion CameraConfig
private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
# endregion AnnotatorConfig
# region AIRecognitionConfig
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
{
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
BigImageTileOverlapPercent = DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT,
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
};
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
private const double TRACKING_PROBABILITY_INCREASE = 15;
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
public const int DETECTION_BATCH_SIZE = 4;
# endregion AIRecognitionConfig
# region GpsDeniedConfig
private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
{
MinKeyPoints = 11
};
# endregion
#region Thumbnails
public static readonly ThumbnailConfig DefaultThumbnailConfig = new()
private static readonly Size DefaultThumbnailSize = new(240, 135);
private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
};
public static readonly Size DefaultThumbnailSize = new(240, 135);
public const int DEFAULT_THUMBNAIL_BORDER = 10;
private const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb";
public const string RESULT_PREFIX = "_result";
#endregion
#region Queue
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
#endregion
#region Database
@@ -113,6 +188,7 @@ public class Constants
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
public const string ADMIN_EMAIL = "admin@azaion.com";
public const string DETECTIONS_TABLENAME = "detections";
public const string MEDIAFILE_TABLENAME = "mediafiles";
#endregion
@@ -124,5 +200,76 @@ public class Constants
#endregion
public const string CSV_PATH = "matches.csv";
public const string SPLIT_SUFFIX = "!split!";
private static readonly InitConfig DefaultInitConfig = new()
{
LoaderClientConfig = DefaultLoaderClientConfig,
InferenceClientConfig = DefaultInferenceClientConfig,
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
},
CameraConfig = DefaultCameraConfig
};
public static readonly AppConfig FailsafeAppConfig = new()
{
AnnotationConfig = DefaultAnnotationConfig,
UIConfig = new UIConfig
{
LeftPanelWidth = DEFAULT_LEFT_PANEL_WIDTH,
RightPanelWidth = DEFAULT_RIGHT_PANEL_WIDTH,
GenerateAnnotatedImage = false
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = DEFAULT_VIDEO_DIR,
ImagesDirectory = DEFAULT_IMAGES_DIR,
LabelsDirectory = DEFAULT_LABELS_DIR,
ResultsDirectory = DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = DEFAULT_THUMBNAILS_DIR,
GpsSatDirectory = DEFAULT_GPS_SAT_DIRECTORY,
GpsRouteDirectory = DEFAULT_GPS_ROUTE_DIRECTORY
},
ThumbnailConfig = DefaultThumbnailConfig,
AIRecognitionConfig = DefaultAIRecognitionConfig,
GpsDeniedConfig = DefaultGpsDeniedConfig,
LoaderClientConfig = DefaultLoaderClientConfig,
InferenceClientConfig = DefaultInferenceClientConfig,
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
CameraConfig = DefaultCameraConfig
};
public static InitConfig ReadInitConfig(ILogger logger)
{
try
{
if (!File.Exists(CONFIG_PATH))
throw new FileNotFoundException(CONFIG_PATH);
var configStr = File.ReadAllText(CONFIG_PATH);
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
return config ?? DefaultInitConfig;
}
catch (Exception e)
{
logger.Error(e, e.Message);
return DefaultInitConfig;
}
}
public static Version GetLocalVersion()
{
var localFileInfo = FileVersionInfo.GetVersionInfo(AZAION_SUITE_EXE);
if (string.IsNullOrWhiteSpace(localFileInfo.ProductVersion))
throw new Exception($"Can't find {AZAION_SUITE_EXE} and its version");
return new Version(localFileInfo.FileVersion!);
}
}
@@ -0,0 +1,71 @@
<UserControl x:Class="Azaion.Common.Controls.CameraConfigControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cfg="clr-namespace:Azaion.Common.DTO.Config"
xmlns:controls="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="120" d:DesignWidth="360">
<Grid Margin="4" Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="65"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="70"/>
</Grid.ColumnDefinitions>
<!-- Altitude -->
<TextBlock Grid.Row="0" Grid.Column="0"
Foreground="LightGray"
VerticalAlignment="Center" Margin="0,0,8,0" Text="Altitude, m:"/>
<Slider x:Name="AltitudeSlider" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Minimum="0" Maximum="10000" TickFrequency="100"
IsSnapToTickEnabled="False"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<controls:NumericUpDown x:Name="AltitudeNud"
Grid.Row="0" Grid.Column="3"
VerticalAlignment="Center"
MinValue="50"
MaxValue="5000"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl},
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Step="10">
</controls:NumericUpDown>
<!-- Focal length -->
<TextBlock
Foreground="LightGray"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Margin="0,8,8,0" Text="Focal length, mm:"/>
<controls:NumericUpDown x:Name="FocalNud"
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
MinValue="0.1"
MaxValue="100"
Step="0.05"
VerticalAlignment="Center"
Value="{Binding Camera.CameraFocalLength, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
<!-- Sensor width -->
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Foreground="LightGray"
Margin="0,8,8,0" Text="Sensor width, mm:"/>
<controls:NumericUpDown x:Name="SensorNud"
Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Step="0.05"
VerticalAlignment="Center"
MinValue="0.1"
MaxValue="100"
Value="{Binding Camera.CameraSensorWidth, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
</Grid>
</UserControl>
@@ -0,0 +1,56 @@
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using Azaion.Common.DTO.Config;
namespace Azaion.Common.Controls;
public partial class CameraConfigControl
{
public static readonly DependencyProperty CameraProperty = DependencyProperty.Register(
nameof(Camera), typeof(CameraConfig), typeof(CameraConfigControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public CameraConfig Camera
{
get => (CameraConfig)GetValue(CameraProperty) ?? new CameraConfig();
set => SetValue(CameraProperty, value);
}
// Fires whenever any camera parameter value changes in UI
public event EventHandler? CameraChanged;
public CameraConfigControl()
{
InitializeComponent();
DataContext = this;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Hook up change notifications
if (AltitudeSlider != null)
AltitudeSlider.ValueChanged += (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty);
SubscribeNud(AltitudeNud);
SubscribeNud(FocalNud);
SubscribeNud(SensorNud);
}
private void SubscribeNud(UserControl? nud)
{
if (nud is NumericUpDown num)
{
var dpd = DependencyPropertyDescriptor.FromProperty(NumericUpDown.ValueProperty, typeof(NumericUpDown));
dpd?.AddValueChanged(num, (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty));
}
}
// Initializes the control with the provided CameraConfig instance and wires two-way binding via dependency property
public void Init(CameraConfig cameraConfig)
{
Camera = cameraConfig;
}
}
+255 -71
View File
@@ -1,13 +1,19 @@
using System.Windows;
using System.Drawing;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Azaion.Annotator.DTO;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
using MediatR;
using Color = System.Windows.Media.Color;
using Image = System.Windows.Controls.Image;
using Point = System.Windows.Point;
using Rectangle = System.Windows.Shapes.Rectangle;
using Size = System.Windows.Size;
namespace Azaion.Common.Controls;
@@ -25,12 +31,17 @@ public class CanvasEditor : Canvas
private Rectangle _curRec = new();
private DetectionControl _curAnn = null!;
private const int MIN_SIZE = 20;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public IMediator Mediator { get; set; } = null!;
private readonly MatrixTransform _matrixTransform = new();
private Point _panStartPoint;
private bool _isZoomedIn;
private const int MIN_SIZE = 12;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform };
private RectangleF? _clampedRect;
public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register(
nameof(GetTimeFunc),
@@ -99,22 +110,61 @@ public class CanvasEditor : Canvas
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
};
KeyDown += (_, args) =>
{
Console.WriteLine($"pressed {args.Key}");
};
MouseDown += CanvasMouseDown;
MouseMove += CanvasMouseMove;
MouseUp += CanvasMouseUp;
SizeChanged += CanvasResized;
Cursor = Cursors.Cross;
Children.Insert(0, BackgroundImage);
Children.Add(_newAnnotationRect);
Children.Add(_horizontalLine);
Children.Add(_verticalLine);
Children.Add(_classNameHint);
Loaded += Init;
RenderTransform = _matrixTransform;
MouseWheel += CanvasWheel;
}
public void SetBackground(ImageSource? source)
{
SetZoom();
BackgroundImage.Source = source;
UpdateClampedRect();
}
private void SetZoom(Matrix? matrix = null)
{
if (matrix == null)
{
_matrixTransform.Matrix = Matrix.Identity;
_isZoomedIn = false;
}
else
{
_matrixTransform.Matrix = matrix.Value;
_isZoomedIn = true;
}
// foreach (var detection in CurrentDetections)
// detection.UpdateAdornerScale(scale: _matrixTransform.Matrix.M11);
}
private void CanvasWheel(object sender, MouseWheelEventArgs e)
{
if (Keyboard.Modifiers != ModifierKeys.Control)
return;
var mousePos = e.GetPosition(this);
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
var matrix = _matrixTransform.Matrix;
if (scale < 1 && matrix.M11 * scale < 1.0)
SetZoom();
else
{
matrix.ScaleAt(scale, scale, mousePos.X, mousePos.Y);
SetZoom(matrix);
}
}
private void Init(object sender, RoutedEventArgs e)
@@ -128,62 +178,167 @@ public class CanvasEditor : Canvas
private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
{
ClearSelections();
NewAnnotationStart(sender, e);
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (Keyboard.Modifiers == ModifierKeys.Control && _isZoomedIn)
{
_panStartPoint = e.GetPosition(this);
SelectionState = SelectionState.PanZoomMoving;
}
else
NewAnnotationStart(sender, e);
(sender as UIElement)?.CaptureMouse();
}
private void CanvasMouseMove(object sender, MouseEventArgs e)
{
var pos = e.GetPosition(this);
var pos = GetClampedPosition(e);
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
_verticalLine.X1 = _verticalLine.X2 = pos.X;
SetLeft(_classNameHint, pos.X + 10);
SetTop(_classNameHint, pos.Y - 30);
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (SelectionState == SelectionState.NewAnnCreating)
NewAnnotationCreatingMove(sender, e);
if (SelectionState == SelectionState.AnnResizing)
AnnotationResizeMove(sender, e);
if (SelectionState == SelectionState.AnnMoving)
AnnotationPositionMove(sender, e);
switch (SelectionState)
{
case SelectionState.NewAnnCreating:
NewAnnotationCreatingMove(pos);
break;
case SelectionState.AnnResizing:
AnnotationResizeMove(pos);
break;
case SelectionState.AnnMoving:
AnnotationPositionMove(pos);
e.Handled = true;
break;
case SelectionState.PanZoomMoving:
PanZoomMove(pos);
break;
}
}
private Point GetClampedPosition(MouseEventArgs e)
{
var pos = e.GetPosition(this);
return !_clampedRect.HasValue
? pos
: new Point
(
Math.Clamp(pos.X, _clampedRect.Value.X, _clampedRect.Value.Right),
Math.Clamp(pos.Y, _clampedRect.Value.Y, _clampedRect.Value.Bottom)
);
}
private void PanZoomMove(Point point)
{
var delta = point - _panStartPoint;
var matrix = _matrixTransform.Matrix;
matrix.Translate(delta.X, delta.Y);
_matrixTransform.Matrix = matrix;
}
private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{
(sender as UIElement)?.ReleaseMouseCapture();
if (SelectionState == SelectionState.NewAnnCreating)
{
var endPos = e.GetPosition(this);
var endPos = GetClampedPosition(e);
_newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0;
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
if (width < MIN_SIZE || height < MIN_SIZE)
return;
var time = GetTimeFunc();
CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
if (width >= MIN_SIZE && height >= MIN_SIZE)
{
Width = width,
Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
Confidence = 1
});
var time = GetTimeFunc();
var control = CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
Left = Math.Min(endPos.X, _newAnnotationStartPos.X),
Top = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
Confidence = 1
});
control.UpdateLayout();
CheckLabelBoundaries(control);
}
}
else if (SelectionState != SelectionState.PanZoomMoving)
CheckLabelBoundaries(_curAnn);
SelectionState = SelectionState.None;
e.Handled = true;
}
private void CheckLabelBoundaries(DetectionControl detectionControl)
{
var lb = detectionControl.DetectionLabelContainer;
var origin = lb.TranslatePoint(new Point(0, 0), this);
lb.Children[0].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var size = lb.Children[0].DesiredSize;
var controlLabel = new RectangleF((float)origin.X, (float)origin.Y, (float)size.Width, (float)size.Height);
foreach (var c in CurrentDetections)
{
if (c == detectionControl)
continue;
var detRect = new RectangleF((float)GetLeft(c), (float)GetTop(c), (float)c.Width, (float)c.Height);
detRect.Intersect(controlLabel);
// var intersect = detections[i].ToRectangle();
// intersect.Intersect(detections[j].ToRectangle());
// detectionControl.
// var otherControls = allControls.Where(c => c != control);
// control.UpdateLabelPosition(otherControls);
}
}
private void CanvasResized(object sender, SizeChangedEventArgs e)
{
_horizontalLine.X2 = e.NewSize.Width;
_verticalLine.Y2 = e.NewSize.Height;
BackgroundImage.Width = e.NewSize.Width;
BackgroundImage.Height = e.NewSize.Height;
UpdateClampedRect();
}
private void UpdateClampedRect()
{
if (BackgroundImage.Source is not BitmapSource imageSource)
{
_clampedRect = null;
return;
}
var imgWidth = imageSource.PixelWidth;
var imgHeight = imageSource.PixelHeight;
var canvasWidth = ActualWidth;
var canvasHeight = ActualHeight;
var imgRatio = imgWidth / (double)imgHeight;
var canvasRatio = canvasWidth / canvasHeight;
double renderedWidth;
double renderedHeight;
if (imgRatio > canvasRatio)
{
renderedWidth = canvasWidth;
renderedHeight = canvasWidth / imgRatio;
}
else
{
renderedHeight = canvasHeight;
renderedWidth = canvasHeight * imgRatio;
}
var xOffset = (canvasWidth - renderedWidth) / 2;
var yOffset = (canvasHeight - renderedHeight) / 2;
_clampedRect = new RectangleF((float)xOffset, (float)yOffset, (float)renderedWidth, (float)renderedHeight);
}
#region Annotation Resizing & Moving
private void AnnotationResizeStart(object sender, MouseEventArgs e)
@@ -192,20 +347,19 @@ public class CanvasEditor : Canvas
_lastPos = e.GetPosition(this);
_curRec = (Rectangle)sender;
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
(sender as UIElement)?.CaptureMouse();
e.Handled = true;
}
private void AnnotationResizeMove(object sender, MouseEventArgs e)
private void AnnotationResizeMove(Point point)
{
if (SelectionState != SelectionState.AnnResizing)
return;
var currentPos = e.GetPosition(this);
var x = GetLeft(_curAnn);
var y = GetTop(_curAnn);
var offsetX = currentPos.X - _lastPos.X;
var offsetY = currentPos.Y - _lastPos.Y;
var offsetX = point.X - _lastPos.X;
var offsetY = point.Y - _lastPos.Y;
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
{
case (HorizontalAlignment.Left, VerticalAlignment.Top):
@@ -245,7 +399,7 @@ public class CanvasEditor : Canvas
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
break;
}
_lastPos = currentPos;
_lastPos = point;
}
private void AnnotationPositionStart(object sender, MouseEventArgs e)
@@ -257,24 +411,31 @@ public class CanvasEditor : Canvas
ClearSelections();
_curAnn.IsSelected = true;
SelectionState = SelectionState.AnnMoving;
e.Handled = true;
}
private void AnnotationPositionMove(object sender, MouseEventArgs e)
private void AnnotationPositionMove(Point point)
{
if (SelectionState != SelectionState.AnnMoving)
return;
var currentPos = e.GetPosition(this);
var offsetX = currentPos.X - _lastPos.X;
var offsetY = currentPos.Y - _lastPos.Y;
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX);
SetTop(_curAnn, GetTop(_curAnn) + offsetY);
_lastPos = currentPos;
e.Handled = true;
var offsetX = point.X - _lastPos.X;
var offsetY = point.Y - _lastPos.Y;
var nextLeft = GetLeft(_curAnn) + offsetX;
var nextTop = GetTop(_curAnn) + offsetY;
if (_clampedRect.HasValue)
{
nextLeft = Math.Clamp(nextLeft, _clampedRect.Value.X, _clampedRect.Value.Right - _curAnn.Width);
nextTop = Math.Clamp(nextTop, _clampedRect.Value.Y, _clampedRect.Value.Bottom - _curAnn.Height);
}
SetLeft(_curAnn, nextLeft);
SetTop(_curAnn, nextTop);
_lastPos = point;
}
#endregion
@@ -284,50 +445,67 @@ public class CanvasEditor : Canvas
private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
{
_newAnnotationStartPos = e.GetPosition(this);
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove;
_newAnnotationRect.MouseMove += (sender, e) =>
{
var currentPos = e.GetPosition(this);
NewAnnotationCreatingMove(currentPos);
};
SelectionState = SelectionState.NewAnnCreating;
}
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e)
private void NewAnnotationCreatingMove(Point point)
{
if (SelectionState != SelectionState.NewAnnCreating)
return;
var currentPos = e.GetPosition(this);
var diff = currentPos - _newAnnotationStartPos;
var diff = point - _newAnnotationStartPos;
_newAnnotationRect.Height = Math.Abs(diff.Y);
_newAnnotationRect.Width = Math.Abs(diff.X);
if (diff.X < 0)
SetLeft(_newAnnotationRect, currentPos.X);
SetLeft(_newAnnotationRect, point.X);
if (diff.Y < 0)
SetTop(_newAnnotationRect, currentPos.Y);
SetTop(_newAnnotationRect, point.Y);
}
public void CreateDetections(TimeSpan time, IEnumerable<Detection> detections, List<DetectionClass> detectionClasses, Size videoSize)
public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
{
foreach (var detection in detections)
foreach (var detection in annotation.Detections)
{
var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
var canvasLabel = new CanvasLabel(detection, RenderSize, videoSize, detection.Confidence);
CreateDetectionControl(detectionClass, time, canvasLabel);
CanvasLabel canvasLabel;
if (!annotation.IsSplit || mediaSize.FitSizeForAI())
canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
else
{
canvasLabel = new CanvasLabel(detection, annotation.SplitTile!.Size, null, detection.Confidence)
.ReframeFromSmall(annotation.SplitTile);
//From CurrentMediaSize to Render Size
var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
}
var control = CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
control.UpdateLayout();
CheckLabelBoundaries(control);
}
}
private void CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
{
var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
detectionControl.MouseDown += AnnotationPositionStart;
SetLeft(detectionControl, canvasLabel.X );
SetTop(detectionControl, canvasLabel.Y);
SetLeft(detectionControl, canvasLabel.Left );
SetTop(detectionControl, canvasLabel.Top);
Children.Add(detectionControl);
CurrentDetections.Add(detectionControl);
_newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
return detectionControl;
}
#endregion
@@ -363,6 +541,12 @@ public class CanvasEditor : Canvas
.ToList();
RemoveAnnotations(expiredAnns);
}
public void ResetBackground() => Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0));
public void ZoomTo(Point point)
{
SetZoom();
var matrix = _matrixTransform.Matrix;
matrix.ScaleAt(2, 2, point.X, point.Y);
SetZoom(matrix);
}
}
@@ -52,6 +52,7 @@
CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public"
PreviewKeyDown="OnKeyBanActivity"
>
<DataGrid.Columns>
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
@@ -86,4 +87,11 @@ public partial class DetectionClasses
{
DetectionDataGrid.SelectedIndex = keyNumber;
}
private void OnKeyBanActivity(object sender, KeyEventArgs e)
{
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
e.Handled = true;
}
}
+54 -32
View File
@@ -5,20 +5,21 @@ using System.Windows.Media;
using System.Windows.Shapes;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
using Label = System.Windows.Controls.Label;
using Annotation = Azaion.Common.Database.Annotation;
namespace Azaion.Common.Controls;
public class DetectionControl : Border
{
private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 12;
private const double RESIZE_RECT_SIZE = 10;
private readonly Grid _grid;
private readonly Label _detectionLabel;
private readonly DetectionLabelPanel _detectionLabelPanel;
public readonly Canvas DetectionLabelContainer;
public TimeSpan Time { get; set; }
private readonly double _confidence;
private List<Rectangle> _resizedRectangles = new();
private readonly List<Rectangle> _resizedRectangles = new();
private DetectionClass _detectionClass = null!;
public DetectionClass DetectionClass
@@ -28,12 +29,11 @@ public class DetectionControl : Border
{
var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
BorderBrush = brush;
BorderThickness = new Thickness(3);
BorderThickness = new Thickness(1);
foreach (var rect in _resizedRectangles)
rect.Stroke = brush;
_detectionLabel.Background = new SolidColorBrush(value.Color.ToConfidenceColor(_confidence));
_detectionLabel.Content = _detectionLabelText(value.UIName);
_detectionLabelPanel.DetectionClass = value;
_detectionClass = value;
}
}
@@ -41,6 +41,7 @@ public class DetectionControl : Border
private readonly Rectangle _selectionFrame;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
@@ -51,37 +52,56 @@ public class DetectionControl : Border
}
}
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)
public void UpdateAdornerScale(double scale)
{
if (Math.Abs(scale) < 0.0001)
return;
var inverseScale = 1.0 / scale;
BorderThickness = new Thickness(4 * inverseScale);
foreach (var rect in _resizedRectangles)
{
rect.Width = 2 * RESIZE_RECT_SIZE * inverseScale;
rect.Height = 2 * RESIZE_RECT_SIZE * inverseScale;;
rect.Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7);
}
}
public (HorizontalAlignment Horizontal, VerticalAlignment Vertical) DetectionLabelPosition
{
get => (DetectionLabelContainer.HorizontalAlignment, DetectionLabelContainer.VerticalAlignment);
set
{
DetectionLabelContainer.HorizontalAlignment = value.Horizontal;
DetectionLabelContainer.VerticalAlignment = value.Vertical;
}
}
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object,
MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
{
Width = canvasLabel.Width;
Height = canvasLabel.Height;
Time = time;
_resizeStart = resizeStart;
_confidence = canvasLabel.Confidence;
var labelContainer = new Canvas
DetectionLabelContainer = new Canvas
{
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
ClipToBounds = false,
Margin = new Thickness(0, 0, 32, 0)
};
_detectionLabel = new Label
_detectionLabelPanel = new DetectionLabelPanel
{
Content = _detectionLabelText(detectionClass.Name),
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -32, 0, 0),
FontSize = 16,
Visibility = Visibility.Visible
Confidence = canvasLabel.Confidence,
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
};
labelContainer.Children.Add(_detectionLabel);
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
_selectionFrame = new Rectangle
{
Margin = new Thickness(-3),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Black),
@@ -106,9 +126,9 @@ public class DetectionControl : Border
VerticalAlignment = VerticalAlignment.Stretch,
Children = { _selectionFrame }
};
_grid.Children.Add(DetectionLabelContainer);
foreach (var rect in _resizedRectangles)
_grid.Children.Add(rect);
_grid.Children.Add(labelContainer);
Child = _grid;
Cursor = Cursors.SizeAll;
@@ -121,23 +141,25 @@ public class DetectionControl : Border
var rect = new Rectangle() // small rectangles at the corners and sides
{
ClipToBounds = false,
Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7),
Margin = new Thickness(-1.1 * RESIZE_RECT_SIZE),
HorizontalAlignment = ha,
VerticalAlignment = va,
Width = RESIZE_RECT_SIZE,
Height = RESIZE_RECT_SIZE,
Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
StrokeThickness = 0.8,
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
Cursor = crs,
Name = name,
};
rect.MouseDown += (sender, args) => _resizeStart(sender, args);
rect.MouseUp += (sender, args) => { (sender as UIElement)?.ReleaseMouseCapture(); };
return rect;
}
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
{
var label = new CanvasLabel(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
return new YoloLabel(label, canvasSize, videoSize);
}
public CanvasLabel ToCanvasLabel() =>
new(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
public YoloLabel ToYoloLabel(Size canvasSize, Size? videoSize = null) =>
new(ToCanvasLabel(), canvasSize, videoSize);
}
@@ -0,0 +1,59 @@
<UserControl x:Class="Azaion.Common.Controls.DetectionLabelPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<UserControl.Resources>
<!-- Friendly (Light Blue Square) -->
<DrawingImage x:Key="Friendly">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="LightBlue" Geometry="M25,50 l150,0 0,100 -150,0 z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- Hostile (Red Diamond) -->
<DrawingImage x:Key="Hostile">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="Red" Geometry="M 100,28 L172,100 100,172 28,100 100,28 Z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- Unknown (Yellow Quatrefoil) -->
<DrawingImage x:Key="Unknown">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="Yellow" Geometry="M63,63 C63,20 137,20 137,63 C180,63 180,137 137,137 C137,180
63,180 63,137 C20,137 20,63 63,63 Z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</UserControl.Resources>
<Grid x:Name="DetectionGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" x:Name="AffiliationImage">
</Image>
<Label Grid.Column="1" x:Name="DetectionClassName" FontSize="16"></Label>
</Grid>
</UserControl>
@@ -0,0 +1,70 @@
using System.Windows.Media;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
namespace Azaion.Common.Controls
{
public partial class DetectionLabelPanel
{
private AffiliationEnum _affiliation = AffiliationEnum.None;
public AffiliationEnum Affiliation
{
get => _affiliation;
set
{
_affiliation = value;
UpdateAffiliationImage();
}
}
private DetectionClass _detectionClass = new();
public DetectionClass DetectionClass {
get => _detectionClass;
set
{
_detectionClass = value;
SetClassName();
}
}
private double _confidence;
public double Confidence
{
get => _confidence;
set
{
_confidence = value;
SetClassName();
}
}
private void SetClassName()
{
DetectionClassName.Content = _confidence >= 0.995 ? _detectionClass.UIName : $"{_detectionClass.UIName}: {_confidence * 100:F0}%";
DetectionGrid.Background = new SolidColorBrush(_detectionClass.Color.ToConfidenceColor(_confidence));
}
public DetectionLabelPanel()
{
InitializeComponent();
}
private string _detectionLabelText(string detectionClassName) =>
_confidence >= 0.98 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%";
private void UpdateAffiliationImage()
{
if (_affiliation == AffiliationEnum.None)
{
AffiliationImage.Source = null;
return;
}
if (TryFindResource(_affiliation.ToString()) is DrawingImage drawingImage)
AffiliationImage.Source = drawingImage;
else
AffiliationImage.Source = null;
}
}
}
+50
View File
@@ -0,0 +1,50 @@
<UserControl x:Class="Azaion.Common.Controls.NumericUpDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="DimGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="12" />
</Grid.RowDefinitions>
<TextBox Name="NudTextBox"
Background="DimGray"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
TextAlignment="Right"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Text="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}"
LostFocus="NudTextBox_OnLostFocus"
PreviewTextInput="NudTextBox_OnPreviewTextInput"
DataObject.Pasting="NudTextBox_OnPasting"
/>
<RepeatButton
Name="NudButtonUp"
Grid.Column="1"
Grid.Row="0"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonUp_OnClick"
>^</RepeatButton>
<RepeatButton
Name="NudButtonDown"
Grid.Column="1"
Grid.Row="1"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonDown_OnClick"
>˅</RepeatButton>
</Grid>
</UserControl>
@@ -0,0 +1,126 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Azaion.Common.Controls;
public partial class NumericUpDown : UserControl
{
public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register(
nameof(MinValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(0m));
public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(
nameof(MaxValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(100m));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
nameof(Value), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m, OnValueChanged));
public static readonly DependencyProperty StepProperty = DependencyProperty.Register(
nameof(Step), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m));
public decimal MinValue
{
get => (decimal)GetValue(MinValueProperty);
set => SetValue(MinValueProperty, value);
}
public decimal MaxValue
{
get => (decimal)GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
public decimal Value
{
get => (decimal)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public decimal Step
{
get => (decimal)GetValue(StepProperty);
set => SetValue(StepProperty, value);
}
public NumericUpDown()
{
InitializeComponent();
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not NumericUpDown control)
return;
control.NudTextBox.Text = ((decimal)e.NewValue).ToString(CultureInfo.InvariantCulture);
control.NudTextBox.SelectionStart = control.NudTextBox.Text.Length;
}
private void NudButtonUp_OnClick(object sender, RoutedEventArgs e)
{
var step = Step <= 0 ? 1m : Step;
var newVal = Math.Min(MaxValue, Value + step);
Value = newVal;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudButtonDown_OnClick(object sender, RoutedEventArgs e)
{
var step = Step <= 0 ? 1m : Step;
var newVal = Math.Max(MinValue, Value - step);
Value = newVal;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudTextBox_OnLostFocus(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(NudTextBox.Text) || !decimal.TryParse(NudTextBox.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
{
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
return;
}
if (number > MaxValue )
{
Value = MaxValue;
NudTextBox.Text = MaxValue.ToString(CultureInfo.InvariantCulture);
}
else if (number < MinValue)
{
Value = MinValue;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
}
else
{
Value = number;
}
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudTextBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
var regex = new Regex("[^0-9.]+");
e.Handled = regex.IsMatch(e.Text);
}
private void NudTextBox_OnPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
var text = (string)e.DataObject.GetData(typeof(string));
var regex = new Regex("[^0-9.]+");
if (regex.IsMatch(text))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using MediatR;
using MessagePack;
namespace Azaion.Common.DTO;
public enum AIAvailabilityEnum
{
None = 0,
Downloading = 10,
Converting = 20,
Uploading = 30,
Enabled = 200,
Warning = 300,
Error = 500
}
[MessagePackObject]
public class AIAvailabilityStatusEvent : INotification
{
[Key("s")] public AIAvailabilityEnum Status { get; set; }
[Key("m")] public string? ErrorMessage { get; set; }
public override string ToString() => $"{StatusMessageDict.GetValueOrDefault(Status, "Помилка")} {ErrorMessage}";
private static readonly Dictionary<AIAvailabilityEnum, string> StatusMessageDict = new()
{
{ AIAvailabilityEnum.Downloading, "Йде завантаження AI для Вашої відеокарти" },
{ AIAvailabilityEnum.Converting, "Йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
{ AIAvailabilityEnum.Uploading, "Йде зберігання AI" },
{ AIAvailabilityEnum.Enabled, "AI готовий для розпізнавання" },
{ AIAvailabilityEnum.Warning, "Неможливо запустити AI наразі, йде налаштування під Ваше залізо" },
{ AIAvailabilityEnum.Error, "Помилка під час налаштування AI" }
};
}
+9
View File
@@ -0,0 +1,9 @@
namespace Azaion.Common.DTO;
public enum AffiliationEnum
{
None = 0,
Friendly = 10,
Hostile = 20,
Unknown = 30
}
+25 -23
View File
@@ -3,31 +3,33 @@ using Azaion.Common.Database;
namespace Azaion.Common.DTO;
public class AnnotationResult
{
public Annotation Annotation { get; set; }
public List<(Color Color, double Confidence)> Colors { get; private set; }
// public class AnnotationResult
//{
//public Annotation Annotation { get; set; }
public string ImagePath { get; set; }
public string TimeStr { get; set; }
public string ClassName { get; set; }
//public string ImagePath { get; set; }
//public string TimeStr { get; set; }
//public List<(Color Color, double Confidence)> Colors { get; private set; }
// public string ClassName { get; set; }
public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
{
// public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
// {
Annotation = annotation;
//Annotation = annotation;
TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
ImagePath = annotation.ImagePath;
//TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
//ImagePath = annotation.ImagePath;
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
Colors = annotation.Detections
.Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
.ToList();
ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
}
}
// var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
// ClassName = detectionClasses.Count > 1
// ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
// : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
//
// Colors = annotation.Detections
// .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
// .ToList();
// }
// }
+7 -2
View File
@@ -7,9 +7,10 @@ using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
{
public Annotation Annotation { get; set; } = annotation;
public bool IsValidator { get; set; } = isValidator;
private BitmapImage? _thumbnail;
public BitmapImage? Thumbnail
@@ -28,7 +29,11 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
public string CreatedEmail => Annotation.CreatedEmail;
public bool IsSeed => IsValidator &&
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
!Annotation.CreatedRole.IsValidator();
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+24
View File
@@ -0,0 +1,24 @@
using CommandLine;
using MessagePack;
namespace Azaion.Common.DTO;
[MessagePackObject]
[Verb("credsManual", HelpText = "Manual Credentials")]
public class ApiCredentials
{
[Key(nameof(Email))]
[Option('e', "email", Required = true, HelpText = "User Email")]
public string Email { get; set; } = null!;
[Key(nameof(Password))]
[Option('p', "pass", Required = true, HelpText = "User Password")]
public string Password { get; set; } = null!;
}
[Verb("credsEncrypted", isDefault: true, HelpText = "Encrypted Credentials")]
public class ApiCredentialsEncrypted
{
[Option('c', "creds", Group = "auto", HelpText = "Encrypted Creds")]
public string Creds { get; set; } = null!;
}
@@ -1,6 +1,6 @@
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
internal class BusinessExceptionDto
public class BusinessExceptionDto
{
public int ErrorCode { get; set; }
public string Message { get; set; } = string.Empty;
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Media;
namespace Azaion.Common.DTO;
public class ClusterDistribution
{
public string Label { get; set; } = "";
public Color Color { get; set; }
public int ClassCount { get; set; }
public double BarWidth { get; set; }
}
@@ -15,5 +15,11 @@ public class AIRecognitionConfig
[Key("d")] public byte[] Data { get; set; } = null!;
[Key("p")] public List<string> Paths { get; set; } = null!;
[Key("m_bs")] public int ModelBatchSize { get; set; } = 2;
[Key("m_bs")] public int ModelBatchSize { get; set; } = 4;
[Key("ov_p")] public double BigImageTileOverlapPercent { get; set; }
[Key("cam_a")] public double Altitude { get; set; }
[Key("cam_fl")] public double CameraFocalLength { get; set; }
[Key("cam_sw")] public double CameraSensorWidth { get; set; }
}
+25 -38
View File
@@ -1,13 +1,14 @@
using System.IO;
using System.Text;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Azaion.Common.Extensions;
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
public class AppConfig
{
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
@@ -25,6 +26,10 @@ public class AppConfig
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public MapConfig MapConfig{ get; set; } = null!;
public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!;
public CameraConfig CameraConfig { get; set; } = null!;
}
public interface IConfigUpdater
@@ -35,53 +40,35 @@ public interface IConfigUpdater
public class ConfigUpdater : IConfigUpdater
{
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public void CheckConfig()
{
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, SecurityConstants.CONFIG_PATH);
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
if (File.Exists(configFilePath))
return;
var appConfig = new AppConfig
{
AnnotationConfig = Constants.DefaultAnnotationConfig,
UIConfig = new UIConfig
{
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
GenerateAnnotatedImage = false
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = Constants.DEFAULT_VIDEO_DIR,
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
},
ThumbnailConfig = Constants.DefaultThumbnailConfig,
AIRecognitionConfig = Constants.DefaultAIRecognitionConfig
};
Save(appConfig);
Save(Constants.FailsafeAppConfig);
}
public void Save(AppConfig config)
{
//Save only user's config
var publicConfig = new
ThrottleExt.Throttle(async () =>
{
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig
};
var publicConfig = new
{
config.LoaderClientConfig,
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig,
config.CameraConfig
};
await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace Azaion.Common.DTO.Config;
public class CameraConfig
{
public decimal Altitude { get; set; }
public decimal CameraFocalLength { get; set; }
public decimal CameraSensorWidth { get; set; }
}
@@ -0,0 +1,6 @@
namespace Azaion.Common.DTO.Config;
public class GpsDeniedConfig
{
public int MinKeyPoints { get; set; }
}
+1
View File
@@ -6,4 +6,5 @@ public class UIConfig
public double RightPanelWidth { get; set; }
public bool GenerateAnnotatedImage { get; set; }
public bool SilentDetection { get; set; }
public bool ShowDatasetWithDetectionsOnly { get; set; }
}
+32
View File
@@ -0,0 +1,32 @@
namespace Azaion.Common.DTO;
public class GeoPoint
{
const double PRECISION_TOLERANCE = 0.00005;
public double Lat { get; }
public double Lon { get; }
public GeoPoint() { }
public GeoPoint(double lat, double lon)
{
Lat = lat;
Lon = lon;
}
public override string ToString() => $"{Lat:F4}, {Lon:F4}";
public override bool Equals(object? obj)
{
if (obj is not GeoPoint point) return false;
return ReferenceEquals(this, obj) || Equals(point);
}
private bool Equals(GeoPoint point) =>
Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE;
public override int GetHashCode() => HashCode.Combine(Lat, Lon);
public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right);
public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right);
}
+2
View File
@@ -12,6 +12,8 @@ public class DetectionClass : ICloneable
public Color Color { get; set; }
public int MaxSizeM { get; set; }
[JsonIgnore]
public string UIName
{
+17
View File
@@ -0,0 +1,17 @@
namespace Azaion.Common.DTO;
public class Direction
{
public double Distance { get; set; }
public double Azimuth { get; set; }
public Direction() { }
public Direction(double distance, double azimuth)
{
Distance = distance;
Azimuth = azimuth;
}
public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg";
}
@@ -1,9 +1,8 @@
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
public class DirectoriesConfig
{
public string ApiResourcesDirectory { get; set; } = null!;
public string? ApiResourcesDirectory { get; set; } = null!;
public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!;
@@ -1,19 +1,22 @@
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.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 LoaderClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
}
public class InferenceClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; }
public string ApiUrl { get; set; } = null!;
}
public class GpsDeniedClientConfig : ExternalClientConfig
{
public int ZeroMqSubscriberPort { get; set; }
public int ZeroMqReceiverPort { get; set; }
}
+5 -5
View File
@@ -1,19 +1,19 @@
using System.Collections.ObjectModel;
using System.Windows;
using Azaion.Common.Database;
namespace Azaion.Common.DTO;
public class FormState
{
public MediaFileInfo? CurrentMedia { get; set; }
public string VideoName => CurrentMedia?.FName ?? "";
public string CurrentMrl { get; set; } = null!;
public Size CurrentVideoSize { get; set; }
public string MediaName => CurrentMedia?.FName ?? "";
public Size CurrentMediaSize { get; set; }
public TimeSpan CurrentVideoLength { get; set; }
public TimeSpan? BackgroundTime { get; set; }
public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public ObservableCollection<Annotation> AnnotationResults { get; set; } = [];
public WindowEnum ActiveWindow { get; set; }
}
-51
View File
@@ -1,51 +0,0 @@
namespace Azaion.Common.DTO;
using System.Collections.Generic;
using System.IO;
public class GpsMatchResult
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
public int KeyPoints { get; set; }
public int Rotation { get; set; }
public string MatchType { get; set; } = null!;
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
{
var imageDatas = new List<GpsMatchResult>();
using var reader = new StreamReader(csvFilePath);
//read header
reader.ReadLine();
if (reader.EndOfStream)
return new List<GpsMatchResult>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line))
continue;
var values = line.Split(',');
if (values.Length == 6)
{
imageDatas.Add(new GpsMatchResult
{
Image = GetFilename(values[0]),
Latitude = double.Parse(values[1]),
Longitude = double.Parse(values[2]),
KeyPoints = int.Parse(values[3]),
Rotation = int.Parse(values[4]),
MatchType = values[5]
});
}
}
return imageDatas;
}
private static string GetFilename(string imagePath) =>
Path.GetFileNameWithoutExtension(imagePath)
.Replace("-small", "");
}
@@ -1,4 +1,4 @@
namespace Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public static class EnumerableExtensions
{
@@ -1,8 +1,12 @@
namespace Azaion.CommonSecurity.DTO;
using Azaion.Common.DTO.Config;
public class SecureAppConfig
namespace Azaion.Common.DTO;
public class InitConfig
{
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public CameraConfig CameraConfig { get; set; } = null!;
}
+55 -42
View File
@@ -22,32 +22,56 @@ public abstract class Label
public class CanvasLabel : Label
{
public double X { get; set; }
public double Y { get; set; }
public double Left { get; set; }
public double Top { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Confidence { get; set; }
public CanvasLabel()
public double Bottom
{
get => Top + Height;
set => Height = value - Top;
}
public CanvasLabel(int classNumber, double x, double y, double width, double height, double confidence = 1) : base(classNumber)
public double Right
{
X = x;
Y = y;
get => Left + Width;
set => Width = value - Left;
}
public double CenterX => Left + Width / 2.0;
public double CenterY => Top + Height / 2.0;
public Size Size => new(Width, Height);
public CanvasLabel() { }
public CanvasLabel(double left, double right, double top, double bottom)
{
Left = left;
Top = top;
Width = right - left;
Height = bottom - top;
Confidence = 1;
ClassNumber = -1;
}
public CanvasLabel(int classNumber, double left, double top, double width, double height, double confidence = 1) : base(classNumber)
{
Left = left;
Top = top;
Width = width;
Height = height;
Confidence = confidence;
}
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double confidence = 1)
public CanvasLabel(YoloLabel label, Size canvasSize, Size? mediaSize = null, double confidence = 1)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height
var videoAr = mediaSize.HasValue
? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr;
ClassNumber = label.ClassNumber;
@@ -60,8 +84,8 @@ public class CanvasLabel : Label
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw;
Y = top * realHeight + blackStripHeight;
Left = left * cw;
Top = top * realHeight + blackStripHeight;
Width = label.Width * cw;
Height = label.Height * realHeight;
}
@@ -70,13 +94,20 @@ public class CanvasLabel : Label
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth;
Y = top * ch;
Left = left * realWidth + blackStripWidth;
Top = top * ch;
Width = label.Width * realWidth;
Height = label.Height * ch;
}
Confidence = confidence;
}
public CanvasLabel ReframeToSmall(CanvasLabel smallTile) =>
new(ClassNumber, Left - smallTile.Left, Top - smallTile.Top, Width, Height, Confidence);
public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) =>
new(ClassNumber, Left + smallTile.Left, Top + smallTile.Top, Width, Height, Confidence);
}
[MessagePackObject]
@@ -105,13 +136,13 @@ public class YoloLabel : Label
public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null)
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? mediaSize = null)
{
var cw = canvasSize.Width;
var ch = canvasSize.Height;
var canvasAr = cw / ch;
var videoAr = videoSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height
var videoAr = mediaSize.HasValue
? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr;
ClassNumber = canvasLabel.ClassNumber;
@@ -119,20 +150,20 @@ public class YoloLabel : Label
double left, top;
if (videoAr > canvasAr) //100% width
{
left = canvasLabel.X / cw;
left = canvasLabel.Left / cw;
Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight;
top = (canvasLabel.Top - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight;
}
else //100% height
{
top = canvasLabel.Y / ch;
top = canvasLabel.Top / ch;
Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth;
left = (canvasLabel.Left - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth;
}
@@ -146,8 +177,11 @@ public class YoloLabel : Label
return null;
var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5)
if (strings.Length < 5)
throw new Exception("Wrong labels format!");
if (strings.Length > 5)
strings = strings[..5];
var res = new YoloLabel
{
@@ -184,24 +218,3 @@ public class YoloLabel : Label
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
}
[MessagePackObject]
public class Detection : YoloLabel
{
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
//For db & serialization
public Detection(){}
public Detection(string annotationName, YoloLabel label, double confidence = 1)
{
AnnotationName = annotationName;
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Confidence = confidence;
}
}
@@ -1,4 +1,4 @@
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
public class LoginResponse
{
@@ -1,27 +1,29 @@
using Azaion.Common.Database;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO.Queue;
using MessagePack;
[MessagePackObject]
public class AnnotationCreatedMessage
public class AnnotationMessage
{
[Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!;
[Key(2)] public string OriginalMediaName { get; set; } = null!;
[Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public byte[] Image { get; set; } = null!;
[Key(7)] public RoleEnum CreatedRole { get; set; }
[Key(8)] public string CreatedEmail { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; }
[Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!;
[Key(2)] public string OriginalMediaName { get; set; } = null!;
[Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public byte[]? Image { get; set; } = null!;
[Key(7)] public RoleEnum Role { get; set; }
[Key(8)] public string Email { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; }
[Key(10)] public AnnotationStatus Status { get; set; }
}
[MessagePackObject]
public class AnnotationValidatedMessage
public class AnnotationBulkMessage
{
[Key(0)] public string Name { get; set; } = null!;
[Key(0)] public string[] AnnotationNames { get; set; } = null!;
[Key(1)] public AnnotationStatus AnnotationStatus { get; set; }
[Key(2)] public string Email { get; set; } = null!;
[Key(3)] public DateTime CreatedDate { get; set; }
}
+58
View File
@@ -0,0 +1,58 @@
using MessagePack;
namespace Azaion.Common.DTO;
[MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
{
[Key("CommandType")]
public CommandType CommandType { get; set; } = commandType;
[Key("Data")]
public byte[]? Data { get; set; } = data;
[Key("Message")]
public string? Message { get; set; } = message;
public static RemoteCommand Create(CommandType commandType) =>
new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data), message);
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
}
[MessagePackObject]
public class LoadFileData(string filename, string? folder = null )
{
[Key(nameof(Folder))]
public string? Folder { get; set; } = folder;
[Key(nameof(Filename))]
public string Filename { get; set; } = filename;
}
public enum CommandType
{
None = 0,
Ok = 3,
Login = 10,
CheckResource = 12,
ListRequest = 15,
ListFiles = 18,
Load = 20,
LoadBigSmall = 22,
UploadBigSmall = 24,
DataBytes = 25,
Inference = 30,
InferenceData = 35,
InferenceStatus = 37,
InferenceDone = 38,
StopInference = 40,
AIAvailabilityCheck = 80,
AIAvailabilityResult = 85,
Error = 90,
Exit = 100,
}
@@ -1,6 +1,4 @@
using Azaion.Common.Extensions;
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
public enum RoleEnum
{
+5 -8
View File
@@ -6,11 +6,8 @@ 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 GeoPoint LeftTop { get; }
public GeoPoint BottomRight { get; }
public string Url { get; set; }
@@ -20,12 +17,12 @@ public class SatTile
Y = y;
Url = url;
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
LeftTop = GeoUtils.TileToWorldPos(x, y, zoom);
BottomRight = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
}
public override string ToString()
{
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
return $"Tile[X={X}, Y={Y}, TL=({LeftTop.Lat:F6}, {LeftTop.Lon:F6}), BR=({BottomRight.Lat:F6}, {BottomRight.Lon:F6})]";
}
}
+3 -2
View File
@@ -1,9 +1,10 @@
namespace Azaion.Annotator.DTO;
namespace Azaion.Common.DTO;
public enum SelectionState
{
None = 0,
NewAnnCreating = 1,
AnnResizing = 2,
AnnMoving = 3
AnnMoving = 3,
PanZoomMoving = 4,
}
@@ -1,4 +1,4 @@
namespace Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
public class User
{
+64 -9
View File
@@ -1,8 +1,7 @@
using System.IO;
using System.Windows.Media;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO;
using MessagePack;
namespace Azaion.Common.Database;
@@ -13,12 +12,14 @@ public class Annotation
private static string _labelsDir = null!;
private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static void InitializeDirs(DirectoriesConfig config)
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
DetectionClassesDict = detectionClassesDict;
}
[Key("n")] public string Name { get; set; } = null!;
@@ -41,12 +42,64 @@ public class Annotation
[Key("lon")]public double Lon { get; set; }
#region Calculated
[IgnoreMember]public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
[IgnoreMember]public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember]public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember]public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
private CanvasLabel? _splitTile;
[IgnoreMember] public CanvasLabel? SplitTile
{
get
{
if (!IsSplit)
return null;
if (_splitTile != null)
return _splitTile;
var startCoordIndex = Name.IndexOf(Constants.SPLIT_SUFFIX, StringComparison.Ordinal) + Constants.SPLIT_SUFFIX.Length;
var coordsStr = Name.Substring(startCoordIndex, 14).Split('_');
_splitTile = new CanvasLabel
{
Left = double.Parse(coordsStr[1]),
Top = double.Parse(coordsStr[2]),
Width = double.Parse(coordsStr[0]),
Height = double.Parse(coordsStr[0])
};
return _splitTile;
}
}
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
private List<(Color Color, double Confidence)>? _colors;
[IgnoreMember] public List<(Color Color, double Confidence)> Colors => _colors ??= Detections
.Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence))
.ToList();
private string? _className;
[IgnoreMember] public string ClassName
{
get
{
if (string.IsNullOrEmpty(_className))
{
var detectionClasses = Detections.Select(x => x.ClassNumber).Distinct().ToList();
_className = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => DetectionClassesDict[x].UIName))
: DetectionClassesDict[detectionClasses.FirstOrDefault()].UIName;
}
return _className;
}
}
#endregion Calculated
}
[MessagePackObject]
@@ -59,5 +112,7 @@ public enum AnnotationStatus
{
None = 0,
Created = 10,
Validated = 20
Edited = 20,
Validated = 30,
Deleted = 40
}
-6
View File
@@ -1,6 +0,0 @@
namespace Azaion.Common.Database;
public class AnnotationName
{
public string Name { get; set; } = null!;
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Database;
public class AnnotationQueueRecord
{
public Guid Id { get; set; }
public DateTime DateTime { get; set; }
public AnnotationStatus Operation { get; set; }
public List<string> AnnotationNames { get; set; } = null!;
}
+3 -2
View File
@@ -1,4 +1,5 @@
using Azaion.Common.DTO;
using CsvHelper;
using LinqToDB;
using LinqToDB.Data;
@@ -7,7 +8,7 @@ namespace Azaion.Common.Database;
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
{
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
public ITable<MediaFile> MediaFiles => this.GetTable<MediaFile>();
}
@@ -0,0 +1,45 @@
using LinqToDB;
using LinqToDB.Mapping;
using Newtonsoft.Json;
namespace Azaion.Common.Database;
public static class AnnotationsDbSchemaHolder
{
public static readonly MappingSchema MappingSchema;
static AnnotationsDbSchemaHolder()
{
MappingSchema = new MappingSchema();
var builder = new FluentMappingBuilder(MappingSchema);
var annotationBuilder = builder.Entity<Annotation>();
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
annotationBuilder
.Ignore(x => x.Milliseconds)
.Ignore(x => x.Classes)
.Ignore(x => x.Classes)
.Ignore(x => x.ImagePath)
.Ignore(x => x.LabelPath)
.Ignore(x => x.ThumbPath);
builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Entity<MediaFile>()
.HasTableName(Constants.MEDIAFILE_TABLENAME);
builder.Build();
}
}
+75 -74
View File
@@ -3,26 +3,26 @@ using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
namespace Azaion.Common.Database;
public interface IDbFactory
{
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func);
void SaveToDisk();
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
Task RunWrite(Func<AnnotationsDb, Task> func);
Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func);
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
}
public class DbFactory : IDbFactory
{
private readonly ILogger<DbFactory> _logger;
private readonly AnnotationConfig _annConfig;
private string MemoryConnStr => "Data Source=:memory:";
@@ -33,8 +33,12 @@ public class DbFactory : IDbFactory
private readonly SQLiteConnection _fileConnection;
private readonly DataOptions _fileDataOptions;
private static readonly SemaphoreSlim WriteSemaphore = new(1, 1);
private static readonly Guid SaveTaskId = Guid.NewGuid();
public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
{
_logger = logger;
_annConfig = annConfig.Value;
_memoryConnection = new SQLiteConnection(MemoryConnStr);
@@ -43,7 +47,7 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
;//.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr);
@@ -53,32 +57,31 @@ public class DbFactory : IDbFactory
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
if (!File.Exists(_annConfig.AnnotationsDbFile))
CreateDb();
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
RecreateTables();
_fileConnection.Open();
using var db = new AnnotationsDb(_fileDataOptions);
var entityTypes = typeof(AnnotationsDb)
.GetProperties()
.Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ITable<>))
.Select(p => p.PropertyType.GetGenericArguments()[0])
.ToArray();
SchemaMigrator.EnsureSchemaUpdated(db, entityTypes);
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
}
private void CreateDb()
private void RecreateTables()
{
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
using var db = new AnnotationsDb(_fileDataOptions);
db.CreateTable<Annotation>();
db.CreateTable<AnnotationName>();
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
}
});
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
db.CreateTable<Annotation>();
if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
db.CreateTable<Detection>();
if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
db.CreateTable<AnnotationQueueRecord>();
}
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
@@ -87,64 +90,62 @@ public class DbFactory : IDbFactory
return await func(db);
}
public async Task Run(Func<AnnotationsDb, Task> func)
public async Task RunWrite(Func<AnnotationsDb, Task> func)
{
await using var db = new AnnotationsDb(_memoryDataOptions);
await func(db);
await WriteSemaphore.WaitAsync();
try
{
await using var db = new AnnotationsDb(_memoryDataOptions);
await func(db);
ThrottleExt.Throttle(async () =>
{
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
finally
{
WriteSemaphore.Release();
}
}
public void SaveToDisk()
public async Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func)
{
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
}
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
{
var names = annotations.Select(x => x.Name).ToList();
await DeleteAnnotations(names, cancellationToken);
await WriteSemaphore.WaitAsync();
try
{
await using var db = new AnnotationsDb(_memoryDataOptions);
var result = await func(db);
ThrottleExt.Throttle(async () =>
{
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
return result;
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
finally
{
WriteSemaphore.Release();
}
}
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
{
await Run(async db =>
await RunWrite(async db =>
{
var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken);
var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations");
_logger.LogInformation($"Deleted {detDeleted} detections, {annDeleted} annotations");
});
SaveToDisk();
}
}
public static class AnnotationsDbSchemaHolder
{
public static readonly MappingSchema MappingSchema;
static AnnotationsDbSchemaHolder()
{
MappingSchema = new MappingSchema();
var builder = new FluentMappingBuilder(MappingSchema);
var annotationBuilder = builder.Entity<Annotation>();
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
annotationBuilder
.Ignore(x => x.Milliseconds)
.Ignore(x => x.Classes)
.Ignore(x => x.Classes)
.Ignore(x => x.ImagePath)
.Ignore(x => x.LabelPath)
.Ignore(x => x.ThumbPath);
builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationName>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME);
builder.Build();
}
}
}
+30
View File
@@ -0,0 +1,30 @@
using Azaion.Common.DTO;
using MessagePack;
using Newtonsoft.Json;
namespace Azaion.Common.Database;
[MessagePackObject]
public class Detection : YoloLabel
{
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
[JsonProperty(PropertyName = "dn")] [Key("dn")] public string Description { get; set; } = null!;
[JsonProperty(PropertyName = "af")] [Key("af")] public AffiliationEnum Affiliation { get; set; }
//For db & serialization
public Detection(){}
public Detection(string annotationName, YoloLabel label, string description = "", double confidence = 1)
{
AnnotationName = annotationName;
Description = description;
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Confidence = confidence;
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Azaion.Common.Database;
public class MediaFile
{
public string Name { get; set; } = null!;
public string MediaUrl { get; set; } = null!;
public DateTime? LastProcessedDate { get; set; }
public MediaStatus Status { get; set; } = MediaStatus.New;
public int? RecognisedObjects { get; set; }
}
public enum MediaStatus
{
None,
New,
AIProcessing,
AIProcessed,
ManualConfirmed,
Error
}
-7
View File
@@ -1,7 +0,0 @@
namespace Azaion.Common.Database;
public class QueueOffset
{
public string QueueName { get; set; } = null!;
public ulong Offset { get; set; }
}
+103
View File
@@ -0,0 +1,103 @@
using System.Data;
using LinqToDB.Data;
using LinqToDB.Mapping;
namespace Azaion.Common.Database;
public static class SchemaMigrator
{
public static void EnsureSchemaUpdated(DataConnection dbConnection, params Type[] entityTypes)
{
var connection = dbConnection.Connection;
var mappingSchema = dbConnection.MappingSchema;
if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
foreach (var type in entityTypes)
{
var entityDescriptor = mappingSchema.GetEntityDescriptor(type);
var tableName = entityDescriptor.Name.Name;
var existingColumns = GetTableColumns(connection, tableName);
if (existingColumns.Count == 0) // table does not exist
{
var columnDefinitions = entityDescriptor.Columns.Select(GetColumnDefinition);
dbConnection.Execute($"CREATE TABLE {tableName} ({string.Join(", ", columnDefinitions)})");
continue;
}
foreach (var column in entityDescriptor.Columns)
{
if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase))
continue;
var columnDefinition = GetColumnDefinition(column);
dbConnection.Execute($"ALTER TABLE {tableName} ADD COLUMN {columnDefinition}");
}
}
}
private static HashSet<string> GetTableColumns(IDbConnection connection, string tableName)
{
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using var cmd = connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({tableName})";
using var reader = cmd.ExecuteReader();
while (reader.Read())
columns.Add(reader.GetString(1)); // "name" is in the second column
return columns;
}
private static string GetColumnDefinition(ColumnDescriptor column)
{
var type = column.MemberType;
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
var sqliteType = GetSqliteType(underlyingType);
var defaultClause = GetSqlDefaultValue(type, underlyingType);
return $"\"{column.ColumnName}\" {sqliteType} {defaultClause}";
}
private static string GetSqliteType(Type type) =>
type switch
{
_ when type == typeof(int)
|| type == typeof(long)
|| type == typeof(bool)
|| type.IsEnum
=> "INTEGER",
_ when type == typeof(double)
|| type == typeof(float)
|| type == typeof(decimal)
=> "REAL",
_ when type == typeof(byte[])
=> "BLOB",
_ => "TEXT"
};
private static string GetSqlDefaultValue(Type originalType, Type underlyingType)
{
var isNullable = originalType.IsClass || Nullable.GetUnderlyingType(originalType) != null;
if (isNullable)
return "NULL";
var defaultValue = Activator.CreateInstance(underlyingType);
if (underlyingType == typeof(bool))
return $"NOT NULL DEFAULT {(Convert.ToBoolean(defaultValue) ? 1 : 0)}";
if (underlyingType.IsEnum)
return $"NOT NULL DEFAULT {(int)(defaultValue ?? 0)}";
if (underlyingType.IsValueType && defaultValue is IFormattable f)
return $"NOT NULL DEFAULT {f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)}";
return $"NOT NULL DEFAULT '{defaultValue}'";
}
}
@@ -3,7 +3,13 @@ using MediatR;
namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
{
public List<Annotation> Annotations { get; set; } = annotations;
public List<string> AnnotationNames { get; set; } = annotationNames;
public bool FromQueue { get; set; } = fromQueue;
}
public class AnnotationAddedEvent(Annotation annotation) : INotification
{
public Annotation Annotation { get; set; } = annotation;
}
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.Events;
public class LoadErrorEvent(string error) : INotification
{
public string Error { get; set; } = error;
}
@@ -0,0 +1,9 @@
using MediatR;
namespace Azaion.Common.Events;
public class SetStatusTextEvent(string text, bool isError = false) : INotification
{
public string Text { get; set; } = text;
public bool IsError { get; set; } = isError;
}
@@ -0,0 +1,3 @@
namespace Azaion.CommonSecurity.Exceptions;
public class BusinessException(string message) : Exception(message);
@@ -1,4 +1,5 @@
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Azaion.Common.Extensions;
@@ -22,4 +23,17 @@ public static class BitmapExtensions
image.Freeze();
return image;
}
public static Color CreateTransparent(this Color color, byte transparency) =>
Color.FromArgb(transparency, color.R, color.G, color.B);
public static async Task SaveImage(this BitmapSource bitmap, string path, CancellationToken ct = default)
{
await using var stream = new FileStream(path, FileMode.Create);
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(stream);
await stream.FlushAsync(ct);
}
}
@@ -0,0 +1,30 @@
namespace Azaion.Common.Extensions;
public static class CancellationTokenExtensions
{
public static void WaitForCancel(this CancellationToken token, TimeSpan timeout)
{
try
{
Task.Delay(timeout, token).Wait(token);
}
catch (OperationCanceledException)
{
//Don't need to catch exception, need only return from the waiting
}
}
public static Task AsTask(this CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
return new TaskCompletionSource<bool>().Task;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
@@ -0,0 +1,12 @@
namespace Azaion.Common.Extensions;
public static class EnumExtensions
{
public static T GetValueOrDefault<T>(this string value, T defaultValue) where T : struct
{
if (string.IsNullOrEmpty(value))
return defaultValue;
return Enum.TryParse(value, true, out T result) ? result : defaultValue;
}
}
+56 -9
View File
@@ -1,4 +1,6 @@
namespace Azaion.Common.Extensions;
using Azaion.Common.DTO;
namespace Azaion.Common.Extensions;
public static class GeoUtils
{
@@ -13,26 +15,71 @@ public static class GeoUtils
return (xTile, yTile);
}
public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom)
public static double ToRadians(double degrees) => degrees * Math.PI / 180.0;
public static double ToDegrees(double radians) => radians * 180.0 / Math.PI;
public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2)
{
var lat1Rad = ToRadians(p1.Lat);
var lat2Rad = ToRadians(p2.Lat);
var dLon = ToRadians(p2.Lon - p1.Lon);
var dLat = ToRadians(p2.Lat - p1.Lat);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Asin(Math.Sqrt(a));
var distance = EARTH_RADIUS * c;
var y = Math.Sin(dLon) * Math.Cos(lat2Rad);
var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon);
var azimuthRadians = Math.Atan2(y, x);
var azimuth = (ToDegrees(azimuthRadians) + 360) % 360;
return new Direction
{
Distance = distance,
Azimuth = azimuth
};
}
public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction)
{
var angularDistance = direction.Distance / EARTH_RADIUS;
var azimuthRadians = ToRadians(direction.Azimuth);
var startLatRad = ToRadians(startPoint.Lat);
var startLonRad = ToRadians(startPoint.Lon);
var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) +
Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians));
var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad),
Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad));
return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad));
}
public static GeoPoint TileToWorldPos(int x, int y, int zoom)
{
var n = Math.Pow(2.0, zoom);
var 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);
return new GeoPoint(latDeg, lonDeg);
}
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
{
var latRad = centerLat * Math.PI / 180.0;
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
var minLat = Math.Max(centerLat - latDiff, -90.0);
var maxLat = Math.Min(centerLat + latDiff, 90.0);
var minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0);
var maxLat = Math.Min(centerGeoPoint.Lat + latDiff, 90.0);
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
var minLon = Math.Max(centerLon - lonDiff, -180.0);
var maxLon = Math.Min(centerLon + lonDiff, 180.0);
var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
return (minLat, maxLat, minLon, maxLon);
}
@@ -0,0 +1,47 @@
using System.Linq.Expressions;
namespace Azaion.Common.Extensions;
public static class QueryableExtensions
{
/// <summary>
/// Adds Where true predicate only if result of condition is true.
/// If false predicate provided, uses it in case of false result
/// Useful for filters, when filters should be applied only when it was set (not NULL)
/// </summary>
public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> query, bool? condition,
Expression<Func<TSource, bool>> truePredicate,
Expression<Func<TSource, bool>>? falsePredicate = null)
{
if (!condition.HasValue)
return query;
if (condition.Value)
return query.Where(truePredicate);
return falsePredicate != null
? query.Where(falsePredicate)
: query;
}
/// <summary>
/// Adds Where true predicate only if result of condition is true.
/// If false predicate provided, uses it in case of false result
/// Useful for filters, when filters should be applied only when it was set (not NULL)
/// </summary>
public static IEnumerable<TSource> WhereIf<TSource>(this IEnumerable<TSource> query, bool? condition,
Func<TSource, bool> truePredicate,
Func<TSource, bool>? falsePredicate = null)
{
if (!condition.HasValue)
return query;
if (condition.Value)
return query.Where(truePredicate);
return falsePredicate != null
? query.Where(falsePredicate)
: query;
}
}
+14
View File
@@ -0,0 +1,14 @@
using Polly;
public static class ResilienceExt
{
public static void WithRetry(this Action operation, int retryCount = 3, int delayMs = 150) =>
Policy.Handle<Exception>()
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
.Execute(operation);
public static TResult WithRetry<TResult>(this Func<TResult> operation, int retryCount = 3, int delayMs = 150) =>
Policy.Handle<Exception>()
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
.Execute(operation);
}
@@ -0,0 +1,10 @@
using System.Windows;
namespace Azaion.Common.Extensions;
public static class SizeExtensions
{
public static bool FitSizeForAI(this Size size) =>
// Allow to be up to FullHD to save as 1280*1280
size.Width <= Constants.AI_TILE_SIZE_DEFAULT * 1.5 && size.Height <= Constants.AI_TILE_SIZE_DEFAULT * 1.5;
}
@@ -54,7 +54,6 @@ public static class ThrottleExt
finally
{
await Task.Delay(interval);
lock (state.StateLock)
{
if (state.CallScheduledDuringCooldown)
+59
View File
@@ -0,0 +1,59 @@
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
namespace Azaion.Common;
public class Security
{
private static string GenDefaultKey()
{
var date = DateTime.UtcNow;
return $"sAzaion_default_dfvkjhg_{date:yyyy}-{date:MM}_{date:dd}_{date:HH}_key";
}
public static string Encrypt<T>(T model, string? key = null) where T : class
{
var json = JsonConvert.SerializeObject(model);
var inputBytes = Encoding.UTF8.GetBytes(json);
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
var iv = RandomNumberGenerator.GetBytes(16);
using var aes = Aes.Create();
aes.Key = keyBytes;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.ISO10126;
using var encryptor = aes.CreateEncryptor();
var ciphertext = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
var result = new byte[iv.Length + ciphertext.Length];
iv.CopyTo(result, 0);
ciphertext.CopyTo(result, iv.Length);
return Convert.ToBase64String(result);
}
public static T Decrypt<T>(string encryptedData, string? key = null) where T : class
{
var ciphertextWithIv = Convert.FromBase64String(encryptedData);
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
var iv = ciphertextWithIv[..16];
var ciphertext = ciphertextWithIv[16..];
using var aes = Aes.Create();
aes.Key = keyBytes;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.ISO10126;
using var decryptor = aes.CreateDecryptor();
var plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
var json = Encoding.UTF8.GetString(plaintext);
return JsonConvert.DeserializeObject<T>(json)!;
}
}
+135 -87
View File
@@ -1,4 +1,5 @@
using System.Drawing.Imaging;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using Azaion.Common.Database;
@@ -7,12 +8,11 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
using LinqToDB;
using LinqToDB.Data;
using MediatR;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using RabbitMQ.Stream.Client;
@@ -20,17 +20,23 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services;
public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
// SHOULD BE ONLY ONE INSTANCE OF AnnotationService. Do not add ANY NotificationHandler to it!
// Queue consumer should be created only once.
public class AnnotationService : IAnnotationService
{
private readonly IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService;
private readonly IMediator _mediator;
private readonly IAzaionApi _api;
private readonly ILogger<AnnotationService> _logger;
private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!;
private readonly UIConfig _uiConfig;
private static readonly Guid SaveTaskId = Guid.NewGuid();
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
public AnnotationService(
IDbFactory dbFactory,
@@ -39,20 +45,22 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
IOptions<UIConfig> uiConfig,
IGalleryService galleryService,
IMediator mediator,
IAzaionApi api)
IAzaionApi api,
ILogger<AnnotationService> logger)
{
_dbFactory = dbFactory;
_producer = producer;
_galleryService = galleryService;
_mediator = mediator;
_api = api;
_logger = logger;
_queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await Init()).Wait();
Task.Run(async () => await InitQueueConsumer()).Wait();
}
private async Task Init(CancellationToken cancellationToken = default)
private async Task InitQueueConsumer(CancellationToken token = default)
{
if (!_api.CurrentUser.Role.IsValidator())
return;
@@ -69,32 +77,59 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
{
Reference = _api.CurrentUser.Email,
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1),
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
MessageHandler = async (_, _, context, message) =>
{
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
offsets.AnnotationsOffset = context.Offset;
ThrottleExt.Throttle(() =>
await _messageProcessingSemaphore.WaitAsync(token);
try
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
return;
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);
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
{
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
{
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
await SaveAnnotationInner(
msg.CreatedDate,
msg.OriginalMediaName,
msg.Name,
msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
msg.Source,
msg.Image == null ? null : new MemoryStream(msg.Image),
msg.Role,
msg.Email,
context.Offset,
token: token);
}
else
{
var msg = MessagePackSerializer.Deserialize<AnnotationBulkMessage>(message.Data.Contents);
if (annotationStatus == AnnotationStatus.Validated)
await ValidateAnnotations(msg.AnnotationNames.ToList(), true, token);
if (annotationStatus == AnnotationStatus.Deleted)
await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), token);
}
}
offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message
ThrottleExt.Throttle(() =>
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
finally
{
_messageProcessingSemaphore.Release();
}
}
});
}
@@ -103,44 +138,50 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
{
a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(),
return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Name, a.Time, a.Detections.ToList(),
SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
}
//Manual
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,
public async Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, annotationName, time, detections, SourceEnum.Manual, stream,
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
// 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,
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, string annotationName, TimeSpan time,
List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole,
string createdEmail,
bool fromQueue = false,
ulong? offset = null,
CancellationToken token = default)
{
AnnotationStatus status;
var fName = originalMediaName.ToTimeName(time);
var annotation = await _dbFactory.Run(async db =>
var status = AnnotationStatus.Created;
var annotation = await _dbFactory.RunWrite(async db =>
{
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
status = userRole.IsValidator() && source == SourceEnum.Manual
? AnnotationStatus.Validated
: AnnotationStatus.Created;
var ann = await db.Annotations
.LoadWith(x => x.Detections)
.FirstOrDefaultAsync(x => x.Name == annotationName, token: token);
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
await db.Detections.DeleteAsync(x => x.AnnotationName == annotationName, token: token);
if (ann != null)
if (ann != null) //Annotation is already exists
{
await db.Annotations
.Where(x => x.Name == fName)
.Set(x => x.Source, source)
status = AnnotationStatus.Edited;
var annotationUpdatable = db.Annotations
.Where(x => x.Name == annotationName)
.Set(x => x.Source, source);
if (userRole.IsValidator() && source == SourceEnum.Manual)
{
annotationUpdatable = annotationUpdatable
.Set(x => x.ValidateDate, createdDate)
.Set(x => x.ValidateEmail, createdEmail);
}
await annotationUpdatable
.Set(x => x.AnnotationStatus, status)
.Set(x => x.CreatedDate, createdDate)
.Set(x => x.CreatedEmail, createdEmail)
.Set(x => x.CreatedRole, userRole)
.UpdateAsync(token: token);
ann.Detections = detections;
}
else
@@ -148,7 +189,7 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
ann = new Annotation
{
CreatedDate = createdDate,
Name = fName,
Name = annotationName,
OriginalMediaName = originalMediaName,
Time = time,
ImageExtension = Constants.JPG_EXT,
@@ -164,36 +205,50 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
return ann;
});
if (stream != null)
//Save image should be done in 1 thread only
await _imageAccessSemaphore.WaitAsync(token);
try
{
var img = System.Drawing.Image.FromStream(stream);
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
Image image = null!;
if (stream != null)
{
image = Image.FromStream(stream);
if (File.Exists(annotation.ImagePath))
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
}
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await _galleryService.CreateThumbnail(annotation, image, token);
if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, image, token);
}
catch (Exception e)
{
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
throw;
}
finally
{
_imageAccessSemaphore.Release();
}
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await _galleryService.CreateThumbnail(annotation, token);
if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, token);
if (!fromQueue && !_uiConfig.SilentDetection) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
ThrottleExt.Throttle(async () =>
{
_dbFactory.SaveToDisk();
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
if (!offset.HasValue) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue([annotation.Name], status, token);
return annotation;
}
public async Task ValidateAnnotations(List<Annotation> annotations, CancellationToken token = default)
public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
{
if (!_api.CurrentUser.Role.IsValidator())
return;
var annNames = annotations.Select(x => x.Name).ToHashSet();
await _dbFactory.Run(async db =>
var annNames = annotationNames.ToHashSet();
await _dbFactory.RunWrite(async db =>
{
await db.Annotations
.Where(x => annNames.Contains(x.Name))
@@ -202,21 +257,14 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
.UpdateAsync(token: token);
});
ThrottleExt.Throttle(async () =>
{
_dbFactory.SaveToDisk();
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
if (!fromQueue)
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
}
}
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);
}
}
public interface IAnnotationService
{
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
}
@@ -1,10 +1,14 @@
using System.Net;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json;
using Azaion.Common.DTO;
namespace Azaion.CommonSecurity.Services;
using Newtonsoft.Json;
using Serilog;
namespace Azaion.Common.Services;
public interface IAzaionApi
{
@@ -14,7 +18,7 @@ public interface IAzaionApi
//Stream GetResource(string filename, string folder);
}
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
public class AzaionApi(ILogger logger, HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
{
private string _jwtToken = null!;
const string APP_JSON = "application/json";
@@ -24,8 +28,8 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
{
get
{
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
() => Get<User>("currentUser"));
var user = cache.GetFromCache(Constants.CURRENT_USER_CACHE_KEY,
() => Get<User>("users/current"));
if (user == null)
throw new Exception("Can't get current user");
return user;
@@ -114,7 +118,7 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia
}
catch (Exception e)
{
Console.WriteLine(e);
logger.Error(e, e.Message);
throw;
}
}
@@ -1,6 +1,6 @@
using LazyCache;
namespace Azaion.CommonSecurity.Services;
namespace Azaion.Common.Services;
public interface ICache
{
+102 -87
View File
@@ -1,14 +1,17 @@
using System.IO;
using System.Net;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using LinqToDB;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.AMQP;
using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services;
@@ -17,17 +20,24 @@ public class FailsafeAnnotationsProducer
{
private readonly ILogger<FailsafeAnnotationsProducer> _logger;
private readonly IDbFactory _dbFactory;
private readonly IAzaionApi _azaionApi;
private readonly QueueConfig _queueConfig;
private readonly UIConfig _uiConfig;
private Producer _annotationProducer = null!;
private Producer _annotationConfirmProducer = null!;
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig)
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger,
IDbFactory dbFactory,
IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IAzaionApi azaionApi)
{
_logger = logger;
_dbFactory = dbFactory;
_azaionApi = azaionApi;
_queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await ProcessQueue());
}
@@ -41,107 +51,112 @@ public class FailsafeAnnotationsProducer
});
}
private async Task Init(CancellationToken cancellationToken = default)
private async Task ProcessQueue(CancellationToken ct = default)
{
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE));
}
private async Task ProcessQueue(CancellationToken cancellationToken = default)
{
await Init(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
while (!ct.IsCancellationRequested)
{
var messages = await GetFromInnerQueue(cancellationToken);
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
{
var sent = false;
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send
{
try
{
var createdMessages = messagesChunk
.Where(x => x.Status == AnnotationStatus.Created)
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
.ToList();
if (createdMessages.Any())
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
var validatedMessages = messagesChunk
.Where(x => x.Status == AnnotationStatus.Validated)
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
.ToList();
if (validatedMessages.Any())
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
await _dbFactory.Run(async db =>
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
sent = true;
_dbFactory.SaveToDisk();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
}
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default)
{
return await _dbFactory.Run(async db =>
{
var annotations = await db.AnnotationsQueue.Join(
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a)
.ToListAsync(token: cancellationToken);
var messages = new List<AnnotationCreatedMessage>();
var badImages = new List<string>();
foreach (var annotation in annotations)
var sent = false;
while (!sent || !ct.IsCancellationRequested) //Waiting for send
{
try
{
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
var annCreateMessage = new AnnotationCreatedMessage
var (records, annotationsDict) = await _dbFactory.Run(async db =>
{
Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time,
CreatedRole = annotation.CreatedRole,
CreatedEmail = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus,
var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
var editedCreatedNames = records
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
.Select(x => x.AnnotationNames.FirstOrDefault())
.ToList();
ImageExtension = annotation.ImageExtension,
Image = image,
Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source,
};
messages.Add(annCreateMessage);
var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
.Where(x => editedCreatedNames.Contains(x.Name))
.ToDictionaryAsync(a => a.Name, token: ct);
return (records, annotationsDict);
});
var messages = new List<Message>();
foreach (var record in records)
{
var appProperties = new ApplicationProperties
{
{ nameof(AnnotationStatus), record.Operation.ToString() },
{ nameof(User.Email), _azaionApi.CurrentUser.Email }
};
if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted))
{
var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage
{
AnnotationNames = record.AnnotationNames.ToArray(),
AnnotationStatus = record.Operation,
Email = _azaionApi.CurrentUser.Email,
CreatedDate = record.DateTime
})) { ApplicationProperties = appProperties };
messages.Add(message);
}
else
{
var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault());
if (annotation == null)
continue;
var image = record.Operation == AnnotationStatus.Created
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
: null;
var annMessage = new AnnotationMessage
{
Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time,
Role = annotation.CreatedRole,
Email = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus,
ImageExtension = annotation.ImageExtension,
Image = image,
Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source,
};
var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties };
messages.Add(message);
}
}
if (messages.Any())
{
await _annotationProducer.Send(messages, CompressionType.Gzip);
var ids = records.Select(x => x.Id).ToList();
var removed = await _dbFactory.RunWrite(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct));
sent = true;
}
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
badImages.Add(annotation.Name);
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
if (badImages.Any())
{
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken);
_dbFactory.SaveToDisk();
}
return messages;
});
}
await Task.Delay(TimeSpan.FromSeconds(5), ct);
}
public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
{
await _dbFactory.Run(async db =>
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
if (_uiConfig.SilentDetection)
return;
await _dbFactory.RunWrite(async db =>
await db.InsertAsync(new AnnotationQueueRecord
{
Id = Guid.NewGuid(),
DateTime = DateTime.UtcNow,
Operation = status,
AnnotationNames = annotationNames
}, token: cancellationToken));
}
}
@@ -1,74 +0,0 @@
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IGpsMatcherService
{
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
void StopGpsMatching();
}
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
{
private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 5;
private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
{
var currentLat = initialLatitude;
var currentLon = initialLongitude;
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
if (Directory.Exists(routeDir))
Directory.Delete(routeDir, true);
Directory.CreateDirectory(routeDir);
var routeFiles = new List<string>();
foreach (var file in Directory.GetFiles(userRouteDir))
{
routeFiles.Add(file);
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
}
var indexOffset = 0;
while (routeFiles.Any())
{
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
gpsMatcherClient.StartMatching(new StartMatchingEvent
{
ImagesCount = POINTS_COUNT,
Latitude = initialLatitude,
Longitude = initialLongitude,
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
RouteDir = dirConfig.Value.GpsRouteDirectory
});
while (true)
{
var result = gpsMatcherClient.GetResult();
if (result == null)
break;
result.Index += indexOffset;
await processResult(result);
currentLat = result.Latitude;
currentLon = result.Longitude;
routeFiles.RemoveAt(0);
}
indexOffset += POINTS_COUNT;
}
}
public void StopGpsMatching()
{
gpsMatcherClient.Stop();
}
}
+24 -24
View File
@@ -9,7 +9,6 @@ using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using LinqToDB;
using LinqToDB.Data;
using Microsoft.Extensions.Logging;
@@ -61,7 +60,7 @@ public class GalleryService(
{
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete();
await dbFactory.Run(async db =>
await dbFactory.RunWrite(async db =>
{
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
@@ -150,14 +149,14 @@ public class GalleryService(
if (!existingAnnotations.ContainsKey(fName))
{
if (missedAnnotations.ContainsKey(fName))
Console.WriteLine($"{fName} is already exists! Duplicate!");
logger.LogInformation($"{fName} is already exists! Duplicate!");
else
missedAnnotations.TryAdd(fName, annotation);
}
if (!thumbnails.Contains(fName))
await CreateThumbnail(annotation, cancellationToken);
await CreateThumbnail(annotation, cancellationToken: cancellationToken);
}
catch (Exception e)
@@ -169,7 +168,7 @@ public class GalleryService(
{
ProgressFn = async num =>
{
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
logger.LogInformation($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
await Task.CompletedTask;
@@ -198,24 +197,23 @@ public class GalleryService(
.Select(x => x.Value)
.ToList();
await dbFactory.Run(async db =>
await dbFactory.RunWrite(async db =>
{
await db.BulkCopyAsync(copyOptions, annotationsToInsert);
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
});
dbFactory.SaveToDisk();
_updateLock.Release();
}
}
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default)
public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default)
{
try
{
var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height;
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
var bitmap = new Bitmap(width, height);
@@ -239,11 +237,11 @@ public class GalleryService(
.ToList();
if (annotation.Detections.Any())
{
var labelsMinX = labels.Min(x => x.X);
var labelsMaxX = labels.Max(x => x.X + x.Width);
var labelsMinX = labels.Min(x => x.Left);
var labelsMaxX = labels.Max(x => x.Left + x.Width);
var labelsMinY = labels.Min(x => x.Y);
var labelsMaxY = labels.Max(x => x.Y + x.Height);
var labelsMinY = labels.Min(x => x.Top);
var labelsMaxY = labels.Max(x => x.Top + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
@@ -272,7 +270,7 @@ public class GalleryService(
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
}
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
@@ -282,10 +280,9 @@ public class GalleryService(
logger.LogError(e, e.Message);
}
}
public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token)
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
{
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
using var g = Graphics.FromImage(originalImage);
foreach (var detection in annotation.Detections)
@@ -294,22 +291,25 @@ public class GalleryService(
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);
g.DrawRectangle(new Pen(brush, width: 3), (float)det.Left, (float)det.Top, (float)det.Width, (float)det.Height);
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black);
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
}
originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg);
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
if (File.Exists(imagePath))
ResilienceExt.WithRetry(() => File.Delete(imagePath));
originalImage.Save(imagePath, ImageFormat.Jpeg);
}
}
public interface IGalleryService
{
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
double ProcessedThumbnailsPercentage { get; set; }
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default);
Task RefreshThumbnails();
Task ClearThumbnails(CancellationToken cancellationToken = default);
Task CreateAnnotatedImage(Annotation annotation, CancellationToken token);
Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default);
}
@@ -0,0 +1,14 @@
using MediatR;
namespace Azaion.Common.Services;
public class GPSMatcherEventHandler(IGpsMatcherService gpsMatcherService) :
INotificationHandler<GPSMatcherResultEvent>,
INotificationHandler<GPSMatcherFinishedEvent>
{
public async Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken) =>
await gpsMatcherService.SetGpsResult(result, cancellationToken);
public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) =>
await gpsMatcherService.FinishGPS(notification, cancellationToken);
}
@@ -0,0 +1,45 @@
using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Common.Services;
public enum MatchTypeEnum
{
None = -1,
MatchTypeSingle = 0,
MatchTypeStitched = 1,
MatchTypeOpticalFlow = 2,
MatchTypeInterpolated = 3,
MatchTypeFailure = 4
}
public class GPSMatcherResultEvent : INotification
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public GeoPoint GeoPoint { get; set; } = null!;
public int KeyPoints { get; set; }
public MatchTypeEnum MatchType { get; set; }
}
public class GPSMatcherResultProcessedEvent : GPSMatcherResultEvent
{
public GeoPoint ProcessedGeoPoint { get; set; } = null!;
public GPSMatcherResultProcessedEvent() { }
public GPSMatcherResultProcessedEvent(GPSMatcherResultEvent gpsMatcherResultEvent, GeoPoint processedGeoPoint)
{
Index = gpsMatcherResultEvent.Index;
Image = gpsMatcherResultEvent.Image;
GeoPoint = gpsMatcherResultEvent.GeoPoint;
KeyPoints = gpsMatcherResultEvent.KeyPoints;
MatchType = gpsMatcherResultEvent.MatchType;
ProcessedGeoPoint = processedGeoPoint;
}
}
public class GPSMatcherJobAcceptedEvent : INotification {}
public class GPSMatcherFinishedEvent : INotification {}
@@ -0,0 +1,113 @@
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using MediatR;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IGpsMatcherService
{
Task RunGpsMatching(string userRouteDir, GeoPoint geoPoint, CancellationToken detectToken = default);
void StopGpsMatching();
Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default);
Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken);
}
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient,
ISatelliteDownloader satelliteTileDownloader,
IOptions<DirectoriesConfig> dirConfig,
IOptions<GpsDeniedConfig> gpsDeniedConfig,
IMediator mediator) : IGpsMatcherService
{
private readonly DirectoriesConfig _dirConfig = dirConfig.Value;
private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 140;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
private const int MAX_AVG_POINTS = 2;
private string _routeDir = "";
private string _userRouteDir = "";
private List<string> _allRouteImages = new();
private Dictionary<string, int> _currentRouteImages = new();
private GeoPoint _lastGeoPoint = new();
private CancellationToken _detectToken;
private int _currentIndex;
private readonly Queue<Direction> _directions = new();
public async Task RunGpsMatching(string userRouteDir, GeoPoint initGeoPoint, CancellationToken detectToken = default)
{
_routeDir = Path.Combine(Constants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory);
_userRouteDir = userRouteDir;
_allRouteImages = Directory.GetFiles(userRouteDir)
.OrderBy(x => x).ToList();
_lastGeoPoint = initGeoPoint;
_detectToken = detectToken;
await StartMatchingRound(0);
}
private async Task StartMatchingRound(int startIndex)
{
//empty route dir
if (Directory.Exists(_routeDir))
Directory.Delete(_routeDir, true);
Directory.CreateDirectory(_routeDir);
_currentRouteImages = _allRouteImages
.Skip(startIndex)
.Take(POINTS_COUNT)
.Select((fullName, index) =>
{
var filename = Path.GetFileName(fullName);
File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename));
return new { Filename = Path.GetFileNameWithoutExtension(fullName), Index = startIndex + index };
})
.ToDictionary(x => x.Filename, x => x.Index);
await satelliteTileDownloader.GetTiles(_lastGeoPoint, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken);
await gpsMatcherClient.StartMatching(new StartMatchingEvent
{
ImagesCount = POINTS_COUNT,
GeoPoint = _lastGeoPoint,
SatelliteImagesDir = _dirConfig.GpsSatDirectory,
RouteDir = _dirConfig.GpsRouteDirectory
});
}
public void StopGpsMatching()
{
gpsMatcherClient.Stop();
}
public async Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default)
{
_currentIndex = _currentRouteImages[result.Image];
_currentRouteImages.Remove(result.Image);
if (result.KeyPoints >= gpsDeniedConfig.Value.MinKeyPoints)
{
var direction = _lastGeoPoint.DirectionTo(result.GeoPoint);
_directions.Enqueue(direction);
if (_directions.Count > MAX_AVG_POINTS)
_directions.Dequeue();
_lastGeoPoint = result.GeoPoint;
}
else
{
var direction = new Direction(_directions.Average(x => x.Distance), _directions.Average(x => x.Azimuth));
_lastGeoPoint = _lastGeoPoint.GoDirection(direction);
}
await mediator.Publish(new GPSMatcherResultProcessedEvent(result, _lastGeoPoint), detectToken);
}
public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken)
{
if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count)
await StartMatchingRound(_currentIndex);
}
}
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.Events;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IGpsMatcherClient : IDisposable
{
Task StartMatching(StartMatchingEvent startEvent);
void Stop();
}
public class StartMatchingEvent
{
public string RouteDir { get; set; } = null!;
public string SatelliteImagesDir { get; set; } = null!;
public int ImagesCount { get; set; }
public GeoPoint GeoPoint { get; set; } = null!;
public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24;
public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{GeoPoint.Lat},{GeoPoint.Lon},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
}
public class GpsMatcherClient : IGpsMatcherClient
{
private readonly IMediator _mediator;
private readonly ILogger<GpsMatcherClient> _logger;
private readonly string _requestAddress;
private readonly RequestSocket _requestSocket = new();
private readonly string _subscriberAddress;
private readonly SubscriberSocket _subscriberSocket = new();
private readonly NetMQPoller _poller = new();
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
{
_mediator = mediator;
_logger = logger;
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.ExternalGpsDeniedPath,
Arguments = $"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
WorkingDirectory = Constants.EXTERNAL_GPS_DENIED_FOLDER,
CreateNoWindow = true
};
process.Start();
}
catch (Exception e)
{
_logger.LogError(e, e.ToString());
}
_requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}";
_requestSocket.Connect(_requestAddress);
_subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqReceiverPort}";
_subscriberSocket.Connect(_subscriberAddress);
_subscriberSocket.Subscribe("");
_subscriberSocket.ReceiveReady += async (sender, e) => await ProcessClientCommand(sender, e);
_poller.Add(_subscriberSocket);
_poller.RunAsync();
}
private async Task ProcessClientCommand(object? sender, NetMQSocketEventArgs e)
{
while (e.Socket.TryReceiveFrameString(TimeSpan.FromMilliseconds(100), out var str))
{
try
{
if (string.IsNullOrEmpty(str))
continue;
switch (str)
{
case "FINISHED":
await _mediator.Publish(new GPSMatcherFinishedEvent());
break;
case "OK":
await _mediator.Publish(new GPSMatcherJobAcceptedEvent());
break;
default:
var parts = str.Split(',');
if (parts.Length != 6)
throw new Exception("Matching Result Failed");
var filename = Path.GetFileNameWithoutExtension(parts[1]);
await _mediator.Publish(new GPSMatcherResultEvent
{
Index = int.Parse(parts[0]),
Image = filename,
GeoPoint = new GeoPoint(double.Parse(parts[2]), double.Parse(parts[3])),
KeyPoints = int.Parse(parts[4]),
MatchType = Enum.TryParse<MatchTypeEnum>(parts[5], out var type) ? type : MatchTypeEnum.None
});
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
}
public async Task StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
_requestSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(300), out var response);
if (response != "OK")
{
_logger.LogError(response);
await _mediator.Publish(new SetStatusTextEvent(response ?? "", true));
}
}
public void Stop() => _requestSocket.SendFrame("STOP");
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_requestSocket.SendFrame("EXIT");
_requestSocket.Disconnect(_requestAddress);
_requestSocket.Dispose();
_subscriberSocket.Disconnect(_subscriberAddress);
_subscriberSocket.Dispose();
}
}
-118
View File
@@ -1,118 +0,0 @@
using System.Diagnostics;
using Azaion.Common.DTO;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IGpsMatcherClient
{
void StartMatching(StartMatchingEvent startEvent);
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class StartMatchingEvent
{
public string RouteDir { get; set; } = null!;
public string SatelliteImagesDir { get; set; } = null!;
public int ImagesCount { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public string ProcessingType { get; set; } = "cuda";
public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24;
public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
}
public class GpsMatcherClient : IGpsMatcherClient
{
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
private readonly RequestSocket _requestSocket = new();
private readonly SubscriberSocket _subscriberSocket = new();
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
{
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.ExternalGpsDeniedPath,
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
//process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
_subscriberSocket.Subscribe("");
}
public void StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
var response = _requestSocket.ReceiveFrameString();
if (response != "OK")
throw new Exception("Start Matching Failed");
}
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retries)
{
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
continue;
if (update == "FINISHED")
return null;
var parts = update.Split(',');
if (parts.Length != 5)
throw new Exception("Matching Result Failed");
return new GpsMatchResult
{
Index = int.Parse(parts[0]),
Image = parts[1],
Latitude = double.Parse(parts[2]),
Longitude = double.Parse(parts[3]),
MatchType = parts[4]
};
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
}
public void Stop()
{
_requestSocket.SendFrame("STOP");
}
}
@@ -0,0 +1,114 @@
using System.Diagnostics;
using System.Text;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using MediatR;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services.Inference;
public interface IInferenceClient : IDisposable
{
void Send(RemoteCommand create);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly ILogger<InferenceClient> _logger;
private readonly DealerSocket _dealer = new();
private readonly NetMQPoller _poller = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
private readonly LoaderClientConfig _loaderClientConfig;
private readonly IMediator _mediator;
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
IMediator mediator,
IOptions<LoaderClientConfig> loaderConfig)
{
_logger = logger;
_inferenceClientConfig = inferenceConfig.Value;
_loaderClientConfig = loaderConfig.Value;
_mediator = mediator;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.EXTERNAL_INFERENCE_PATH,
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
CreateNoWindow = true
};
process.Start();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket);
_poller.Add(_dealer);
_ = Task.Run(() => _poller.RunAsync());
}
private async Task ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default)
{
while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes))
{
if (bytes.Length == 0)
continue;
var remoteCommand = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
switch (remoteCommand.CommandType)
{
case CommandType.InferenceData:
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
break;
case CommandType.InferenceStatus:
var statusEvent = MessagePackSerializer.Deserialize<InferenceStatusEvent>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(statusEvent, ct);
break;
case CommandType.InferenceDone:
await _mediator.Publish(new InferenceDoneEvent(), ct);
break;
case CommandType.AIAvailabilityResult:
var aiAvailabilityStatus = MessagePackSerializer.Deserialize<AIAvailabilityStatusEvent>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(aiAvailabilityStatus, ct);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public void Stop() =>
Send(RemoteCommand.Create(CommandType.StopInference));
public void Send(RemoteCommand command) =>
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.Close();
_dealer.Dispose();
}
}
@@ -0,0 +1,64 @@
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services.Inference;
public interface IInferenceService
{
Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default);
CancellationTokenSource InferenceCancelTokenSource { get; set; }
CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; }
void StopInference();
}
// SHOULD BE ONLY ONE INSTANCE OF InferenceService. Do not add ANY NotificationHandler to it!
// _inferenceCancelTokenSource should be created only once.
public class InferenceService : IInferenceService
{
private readonly IInferenceClient _client;
private readonly IAzaionApi _azaionApi;
private readonly IOptions<AIRecognitionConfig> _aiConfigOptions;
public InferenceService(IInferenceClient client,
IAzaionApi azaionApi,
IOptions<AIRecognitionConfig> aiConfigOptions)
{
_client = client;
_azaionApi = azaionApi;
_aiConfigOptions = aiConfigOptions;
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
}
public CancellationTokenSource InferenceCancelTokenSource { get; set; } = new();
public CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } = new();
private async Task CheckAIAvailabilityStatus()
{
CheckAIAvailabilityTokenSource = new CancellationTokenSource();
while (!CheckAIAvailabilityTokenSource.IsCancellationRequested)
{
_client.Send(RemoteCommand.Create(CommandType.AIAvailabilityCheck));
await Task.Delay(10000, CheckAIAvailabilityTokenSource.Token);
}
}
public async Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default)
{
InferenceCancelTokenSource = new CancellationTokenSource();
_client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials));
var aiConfig = _aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
aiConfig.Altitude = (double)cameraConfig.Altitude;
aiConfig.CameraFocalLength = (double)cameraConfig.CameraFocalLength;
aiConfig.CameraSensorWidth = (double)cameraConfig.CameraSensorWidth;
_client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, InferenceCancelTokenSource.Token);
await combinedTokenSource.Token.AsTask();
}
public void StopInference() => _client.Stop();
}
@@ -0,0 +1,31 @@
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.Events;
using MediatR;
using MessagePack;
using Microsoft.Extensions.Logging;
namespace Azaion.Common.Services.Inference;
public class InferenceServiceEventHandler(IInferenceService inferenceService, IAnnotationService annotationService, IMediator mediator) :
INotificationHandler<InferenceDataEvent>,
INotificationHandler<InferenceStatusEvent>,
INotificationHandler<InferenceDoneEvent>
{
public async Task Handle(InferenceDataEvent e, CancellationToken ct)
{
var annotation = await annotationService.SaveAnnotation(e.AnnotationImage, ct);
await mediator.Publish(new AnnotationAddedEvent(annotation), ct);
}
public async Task Handle(InferenceStatusEvent e, CancellationToken ct)
{
await mediator.Publish(new SetStatusTextEvent($"{e.MediaName}: {e.DetectionsCount} detections"), ct);
}
public async Task Handle(InferenceDoneEvent notification, CancellationToken cancellationToken)
{
await inferenceService.InferenceCancelTokenSource.CancelAsync();
}
}
@@ -0,0 +1,22 @@
using Azaion.Common.Database;
using MediatR;
using MessagePack;
namespace Azaion.Common.Services.Inference;
public class InferenceDataEvent(AnnotationImage annotationImage) : INotification
{
public AnnotationImage AnnotationImage { get; set; } = annotationImage;
}
[MessagePackObject]
public class InferenceStatusEvent : INotification
{
[Key("mn")]
public string? MediaName { get; set; }
[Key("dc")]
public int DetectionsCount { get; set; }
}
public class InferenceDoneEvent : INotification;
@@ -1,57 +0,0 @@
using System.Text;
using Azaion.Common.Database;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.Services;
using MessagePack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IInferenceService
{
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default);
void StopInference();
}
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IAzaionApi azaionApi, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
{
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
{
client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials));
var aiConfig = aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
while (!detectToken.IsCancellationRequested)
{
try
{
var bytes = client.GetBytes(ct: detectToken);
if (bytes == null)
throw new Exception("Can't get bytes from inference client");
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
return;
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken);
await processAnnotation(annotationImage);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
break;
}
}
}
public void StopInference()
{
client.Send(RemoteCommand.Create(CommandType.StopInference));
}
}
+94
View File
@@ -0,0 +1,94 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using Azaion.Common.DTO;
using MessagePack;
using NetMQ;
using NetMQ.Sockets;
using Serilog;
using Exception = System.Exception;
namespace Azaion.Common.Services;
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
public void StartClient()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.EXTERNAL_LOADER_PATH,
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
CreateNoWindow = true
};
process.Start();
}
catch (Exception e)
{
logger.Error(e, e.Message);
throw;
}
}
public void Connect()
{
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
}
public void Login(ApiCredentials credentials)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Login, credentials));
if (result.CommandType != CommandType.Ok)
throw new Exception(result.Message);
}
public MemoryStream LoadFile(string filename, string folder)
{
var result = SendCommand(RemoteCommand.Create(CommandType.Load, new LoadFileData(filename, folder)));
if (result.Data?.Length == 0)
throw new Exception($"Can't load {filename}. Returns 0 bytes");
return new MemoryStream(result.Data!);
}
private RemoteCommand SendCommand(RemoteCommand command, int retryCount = 50, int retryDelayMs = 800)
{
try
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
{
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
continue;
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
if (res.CommandType == CommandType.Error)
throw new Exception(res.Message);
return res;
}
throw new Exception($"Sent {command} {retryCount} times, with wait time {retryDelayMs}ms for each call. No response from client.");
}
catch (Exception e)
{
logger.Error(e, e.Message);
throw;
}
}
public void Stop()
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
}
public void Dispose()
{
_dealer.Dispose();
}
}
+25 -12
View File
@@ -5,9 +5,10 @@ using System.Net.Http;
using System.Net.Http.Json;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
@@ -19,14 +20,15 @@ namespace Azaion.Common.Services;
public interface ISatelliteDownloader
{
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
Task GetTiles(GeoPoint geoPoint, double radiusM, int zoomLevel, CancellationToken token = default);
}
public class SatelliteDownloader(
ILogger<SatelliteDownloader> logger,
IOptions<MapConfig> mapConfig,
IOptions<DirectoriesConfig> directoriesConfig,
IHttpClientFactory httpClientFactory)
IHttpClientFactory httpClientFactory,
IMediator mediator)
: ISatelliteDownloader
{
private const int INPUT_TILE_SIZE = 256;
@@ -41,19 +43,30 @@ public class SatelliteDownloader(
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);
private readonly string _satDirectory = Path.Combine(Constants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
public async Task GetTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default)
{
await mediator.Publish(new SetStatusTextEvent(
$"Завантажуються супутникові зображення по координатах: центр: " +
$"lat: {centerGeoPoint.Lat:F3} lon: {centerGeoPoint.Lon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token);
//empty Satellite directory
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);
var dtRes = await DownloadTiles(centerGeoPoint, radiusM, zoomLevel, token);
await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token);
var image = ComposeTiles(dtRes.Tiles, token);
if (image == null)
return;
// Save big map. Uncomment when MapHandler with custom pick images would be ready
// var outputFilename = Path.Combine(_satDirectory,
// $"map_tl_{dtRes.LatMax:F6}_{dtRes.LonMin:F6}_br_{dtRes.LatMin:F6}_{dtRes.LonMax:F6}.tif"
// );
// await image.SaveAsTiffAsync(outputFilename, token);
await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token);
await SplitToTiles(image, dtRes, token);
}
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
@@ -140,7 +153,7 @@ public class SatelliteDownloader(
}
catch (Exception)
{
Console.WriteLine($"Error while loading tile: {tileData}");
logger.LogError($"Error while loading tile: {tileData}");
}
if (token.IsCancellationRequested)
return;
@@ -172,9 +185,9 @@ public class SatelliteDownloader(
}
}
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
private async Task<DownloadTilesResult> DownloadTiles(GeoPoint centerGeoPoint, double radiusM, int zoomLevel, CancellationToken token = default)
{
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM);
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerGeoPoint, radiusM);
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
+71
View File
@@ -0,0 +1,71 @@
using System.Windows;
using System.Windows.Media.Imaging;
using Azaion.Common.DTO;
namespace Azaion.Common.Services;
public class TileResult(CanvasLabel tile, List<CanvasLabel> detections)
{
public CanvasLabel Tile { get; set; } = tile;
public List<CanvasLabel> Detections { get; set; } = detections;
}
public static class TileProcessor
{
public const int BORDER = 10;
public static List<TileResult> Split(Size originalSize, List<CanvasLabel> detections, CancellationToken cancellationToken)
{
var results = new List<TileResult>();
var processingDetectionList = new List<CanvasLabel>(detections);
while (processingDetectionList.Count > 0 && !cancellationToken.IsCancellationRequested)
{
var topMostDetection = processingDetectionList
.OrderBy(d => d.Top)
.First();
var result = GetDetectionsInTile(originalSize, topMostDetection, processingDetectionList);
processingDetectionList.RemoveAll(x => result.Detections.Contains(x));
results.Add(result);
}
return results;
}
private static TileResult GetDetectionsInTile(Size originalSize, CanvasLabel startDet, List<CanvasLabel> allDetections)
{
var tile = new CanvasLabel(startDet.Left, startDet.Right, startDet.Top, startDet.Bottom);
var maxSize = new List<double> { startDet.Width + BORDER, startDet.Height + BORDER, Constants.AI_TILE_SIZE_DEFAULT }.Max();
var selectedDetections = new List<CanvasLabel>{startDet};
foreach (var det in allDetections)
{
if (det == startDet)
continue;
var commonTile = new CanvasLabel(
left: Math.Min(tile.Left, det.Left),
right: Math.Max(tile.Right, det.Right),
top: Math.Min(tile.Top, det.Top),
bottom: Math.Max(tile.Bottom, det.Bottom)
);
if (commonTile.Width + BORDER > maxSize || commonTile.Height + BORDER > maxSize)
continue;
tile = commonTile;
selectedDetections.Add(det);
}
// boundary-aware centering
var centerX = selectedDetections.Average(x => x.CenterX);
var centerY = selectedDetections.Average(d => d.CenterY);
tile.Width = maxSize;
tile.Height = maxSize;
tile.Left = Math.Max(0, Math.Min(originalSize.Width - maxSize, centerX - tile.Width / 2.0));
tile.Top = Math.Max(0, Math.Min(originalSize.Height - maxSize, centerY - tile.Height / 2.0));
return new TileResult( tile, selectedDetections);
}
}
@@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="NetMQ" Version="4.0.1.13" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>
</Project>
@@ -1,13 +0,0 @@
using MessagePack;
namespace Azaion.CommonSecurity.DTO;
[MessagePackObject]
public class ApiCredentials(string email, string password) : EventArgs
{
[Key(nameof(Email))]
public string Email { get; set; } = email;
[Key(nameof(Password))]
public string Password { get; set; } = password;
}
@@ -1,9 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class HardwareInfo
{
public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!;
}
@@ -1,55 +0,0 @@
using Azaion.CommonSecurity.DTO;
namespace Azaion.CommonSecurity;
public class SecurityConstants
{
public const string CONFIG_PATH = "config.json";
public const string DUMMY_DIR = "dummy";
#region ExternalClientsConfig
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
public const int DEFAULT_RETRY_COUNT = 25;
public const int DEFAULT_TIMEOUT_SECONDS = 5;
# region Cache keys
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string HARDWARE_INFO_KEY = "HardwareInfo";
# endregion
public static readonly SecureAppConfig DefaultSecureAppConfig = new()
{
InferenceClientConfig = new InferenceClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT
},
GpsDeniedClientConfig = new GpsDeniedClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT,
},
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
}
};
#endregion ExternalClientsConfig
}
@@ -1,104 +0,0 @@
using System.Diagnostics;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.CommonSecurity.Services;
public interface IInferenceClient
{
void Send(RemoteCommand create);
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> config)
{
_inferenceClientConfig = config.Value;
Start();
_ = Task.Run(ProcessClientCommands);
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
}
private async Task ProcessClientCommands()
{
//TODO: implement always on ready to client's requests. Utilize RemoteCommand
await Task.CompletedTask;
}
public void Stop()
{
if (!_dealer.IsDisposed)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Close();
}
}
public void Send(RemoteCommand command)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
}
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
{
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
}
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum < retries)
{
tryNum++;
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
continue;
return bytes;
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each");
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More