225 Commits

Author SHA1 Message Date
dzaitsev 6fce10eb39 added zip.bat to pack build artefacts for both Full and Iterative installers. 2025-05-21 14:05:41 +03:00
dzaitsev 4456e52207 pipelines zip and gdrive upload
updated
2025-05-07 20:14:44 +03:00
dzaitsev 16c264ec9a pipelines zip and gdrive upload
updated
2025-05-07 20:11:54 +03:00
dzaitsev cea1c69fec pipelines zip and gdrive upload
updated
2025-05-07 20:11:05 +03:00
dzaitsev 6e1e6d903d pipelines zip and gdrive upload
updated
2025-05-07 20:09:38 +03:00
dzaitsev 46ec7fddc4 pipelines zip and gdrive upload
updated
2025-05-07 20:06:08 +03:00
dzaitsev dd5521dc3b pipelines zip and gdrive upload
updated
2025-05-07 20:03:51 +03:00
dzaitsev bfeb888c55 pipelines zip and gdrive upload
updated
2025-05-07 19:59:57 +03:00
dzaitsev 9ae9265ff6 pipelines zip and gdrive upload
updated
2025-05-07 19:58:24 +03:00
dzaitsev f2c5490ff3 pipelines zip and gdrive upload
updated
2025-05-07 19:53:13 +03:00
dzaitsev 27b90f2a71 pipelines zip and gdrive upload
updated
2025-05-07 19:49:16 +03:00
dzaitsev 8df405c898 pipelines zip and gdrive upload
updated
2025-05-07 19:48:38 +03:00
dzaitsev e0fd55d908 pipelines zip and gdrive upload
updated
2025-05-07 19:38:49 +03:00
dzaitsev d1520109ef pipelines zip and gdrive upload
updated
2025-05-07 19:36:40 +03:00
dzaitsev 81620888f2 pipelines zip and gdrive upload
updated
2025-05-07 19:33:44 +03:00
dzaitsev 5c3fa80a17 pipelines zip and gdrive upload
updated
2025-05-07 19:31:28 +03:00
dzaitsev e82c3f5bcb pipelines zip and gdrive upload
updated
2025-05-07 19:27:36 +03:00
dzaitsev f7dd087fad pipelines zip and gdrive upload
updated
2025-05-07 19:25:30 +03:00
dzaitsev 64c99d88ec pipelines zip and gdrive upload
updated
2025-05-07 19:20:27 +03:00
dzaitsev 7fff1d9af4 pipelines zip and gdrive upload
updated
2025-05-07 19:14:09 +03:00
dzaitsev 66632a97c1 pipelines zip and gdrive upload
updated
2025-05-07 19:10:55 +03:00
dzaitsev e6ab8bde47 pipelines zip and gdrive upload
updated
2025-05-07 19:06:57 +03:00
dzaitsev 80e1877c18 pipelines zip and gdrive upload
updated
2025-05-07 18:49:17 +03:00
dzaitsev e6ec904657 updated pipelines zip and gdrive upload 2025-05-07 18:41:44 +03:00
dzaitsev 495268e56a updated pipelines zip and gdrive upload 2025-05-07 18:38:56 +03:00
dzaitsev c3e4b741c8 updated pipelines zip and gdrive upload 2025-05-07 18:37:26 +03:00
dzaitsev 5c87f536c1 updated zip pipeline to use parameters from Azaion pipeline - fixed path 2025-05-07 18:31:55 +03:00
dzaitsev d5e7a28964 updated zip pipeline to use parameters from Azaion pipeline. 2025-05-07 18:22:51 +03:00
dzaitsev 961c750f1f latest fixes for Gdrive upload 2025-05-04 18:52:08 +03:00
dzaitsev 1acfab9b87 latest fixes for Gdrive upload 2025-05-04 18:49:25 +03:00
dzaitsev 2766be732d latest fixes for Gdrive upload 2025-05-04 15:39:03 +03:00
dzaitsev bb7eec1d5d latest fixes for Gdrive upload 2025-05-04 15:33:23 +03:00
dzaitsev 7fc51e19ee latest fixes for Gdrive upload 2025-05-04 15:31:12 +03:00
dzaitsev 4703c73a24 gdrive update1 2025-05-04 15:29:00 +03:00
dzaitsev 61ca5c2899 gdrive update1 2025-05-04 15:26:35 +03:00
dzaitsev b776576b76 gdrive update1 2025-05-04 15:25:36 +03:00
dzaitsev eddd3f2052 gdrive update1 2025-05-04 15:19:34 +03:00
dzaitsev 838cd1b8ec gdrive update1 2025-05-04 15:18:50 +03:00
dzaitsev bfe620d85e gdrive update1 2025-05-04 15:15:58 +03:00
dzaitsev 0602b0c6f2 Revert "gdrive update1"
This reverts commit a1f46799
2025-05-04 15:15:23 +03:00
dzaitsev d547a4007e gdrive update1 2025-05-04 15:14:12 +03:00
dzaitsev a1f46799c7 gdrive update1 2025-05-04 15:11:06 +03:00
dzaitsev ccc598d746 gdrive update1 2025-05-04 15:09:01 +03:00
dzaitsev c83ed53672 gdrive update1 2025-05-04 15:06:53 +03:00
dzaitsev 7985c298a6 gdrive update 2025-05-04 15:04:04 +03:00
dzaitsev 7343e9d630 gdrive update 2025-05-04 15:02:27 +03:00
dzaitsev f42646bc72 gdrive update 2025-05-04 15:00:58 +03:00
dzaitsev 78caeac943 gdrive update 2025-05-04 14:59:16 +03:00
dzaitsev efde78ad93 gdrive update 2025-05-04 14:58:19 +03:00
dzaitsev 5d0e49b6fd gdrive update 2025-05-04 14:54:08 +03:00
dzaitsev 514c275f3a gdrive update 2025-05-04 14:44:44 +03:00
dzaitsev 41c2ed37c1 gdrive update 2025-05-04 14:43:16 +03:00
dzaitsev 8574e10b52 gdrive update 2025-05-04 14:41:37 +03:00
dzaitsev 44bef40d9b gdrive update 2025-05-04 14:39:47 +03:00
dzaitsev f0f6e05b0d gdrive update 2025-05-04 14:34:33 +03:00
dzaitsev 0c0cc1bb83 gdrive update 2025-05-04 14:25:28 +03:00
dzaitsev 24324b5ffd zip update 2025-05-04 14:21:29 +03:00
dzaitsev 71e8f088a0 zip update 2025-05-04 14:19:02 +03:00
dzaitsev 08f93c775b zip update 2025-05-04 14:16:49 +03:00
dzaitsev 98d99ec7be zip update 2025-05-04 14:14:28 +03:00
dzaitsev 604e3d132e zip update 2025-05-04 14:13:16 +03:00
dzaitsev 366aab294e zip update 2025-05-04 14:11:15 +03:00
dzaitsev 154b5cdcf6 zip update 2025-05-04 14:08:46 +03:00
dzaitsev 6c49b63e8e zip update 2025-05-04 14:04:15 +03:00
dzaitsev 6be07693cc zip update 2025-05-04 14:02:24 +03:00
dzaitsev c1a755c477 zip update 2025-05-04 14:00:34 +03:00
dzaitsev 23f9ff16a4 zip update 2025-05-04 13:59:44 +03:00
dzaitsev b165aa3ede zip update 2025-05-04 13:58:40 +03:00
dzaitsev 05b830b7fb zip update 2025-05-04 13:56:31 +03:00
dzaitsev 5b143d38ce zip update 2025-05-04 13:55:45 +03:00
dzaitsev 53ecfb3cad zip update 2025-05-04 13:55:15 +03:00
dzaitsev fcda29fd49 zip update 2025-05-04 13:54:32 +03:00
dzaitsev d5057dd86c zip update 2025-05-04 13:53:29 +03:00
dzaitsev 7de0fe363b zip update 2025-05-04 13:52:20 +03:00
dzaitsev ea71ec2add zip update 2025-05-04 13:50:56 +03:00
dzaitsev f100ed638d zip update 2025-05-04 13:48:21 +03:00
dzaitsev 30d97c6244 zip update 2025-05-04 13:40:28 +03:00
dzaitsev 1277ebffc6 zip update 2025-05-04 13:38:15 +03:00
dzaitsev 926345efaf zip update 2025-05-04 13:37:18 +03:00
dzaitsev 348eca5080 zip update 2025-05-04 13:31:50 +03:00
dzaitsev 2d93813519 zip update 2025-05-04 13:30:11 +03:00
dzaitsev c999acd120 gdrive update
zip update
2025-05-04 13:27:45 +03:00
dzaitsev 44769ca422 gdrive update
zip update
2025-05-04 13:25:55 +03:00
dzaitsev 6a336e90c5 added zip 2025-05-04 13:24:13 +03:00
dzaitsev 2ad40e165d 1111 2025-05-04 13:21:46 +03:00
dzaitsev 83e5eb04e3 Revert "new" .. revert
This reverts commit 9becf663
2025-05-04 13:08:28 +03:00
dzaitsev c5df76e8ce added stage to zip install kit files 2025-05-04 11:32:59 +03:00
dzaitsev ea9730ff3e added stage to zip install kit files 2025-05-03 15:18:13 +03:00
dzaitsev 6f00dfdd42 added GDrive Upload pipeline 2025-05-03 15:12:30 +03:00
dzaitsev 6d17632c23 updating and testing ImageMatcher pipeline 2025-05-03 15:09:28 +03:00
dzaitsev 0153634166 updating and testing ImageMatcher pipeline 2025-05-03 14:24:48 +03:00
dzaitsev cc86ba795b updating and testing ImageMatcher pipeline 2025-05-03 14:18:44 +03:00
dzaitsev 0327459d1a updating and testing ImageMatcher pipeline 2025-05-03 14:18:19 +03:00
dzaitsev 0dbf25be65 updating and testing ImageMatcher pipeline 2025-05-03 14:15:59 +03:00
dzaitsev 4c12a3244f updating and testing ImageMatcher pipeline 2025-05-03 14:14:21 +03:00
dzaitsev 42f1720172 updating and testing ImageMatcher pipeline 2025-05-03 14:11:18 +03:00
dzaitsev 08e6e2e0c3 updating and testing ImageMatcher pipeline 2025-05-03 14:09:03 +03:00
dzaitsev 4ee901e533 updating and testing ImageMatcher pipeline 2025-05-03 14:07:19 +03:00
dzaitsev 99f3a973a8 updating and testing ImageMatcher pipeline 2025-05-03 14:06:09 +03:00
dzaitsev 5d44eb89e1 updating and testing ImageMatcher pipeline 2025-05-03 14:05:41 +03:00
dzaitsev e01b7e4eac updating and testing ImageMatcher pipeline 2025-05-03 13:55:13 +03:00
dzaitsev 66cc529ffe updating and testing ImageMatcher pipeline 2025-05-03 13:53:41 +03:00
dzaitsev edfdc00807 updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:52:58 +03:00
dzaitsev 994528717c updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:42:31 +03:00
dzaitsev e6be7d6d15 updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:41:20 +03:00
dzaitsev 40f394ac3e updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:38:54 +03:00
dzaitsev bdd4262d50 updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:37:49 +03:00
dzaitsev e6bfb221e7 updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:35:26 +03:00
dzaitsev 970daf84db updating and testing BuildDependencies Jenkins pipeline file 2025-05-03 13:31:39 +03:00
dzaitsev 756806bdeb added BuildDependencies Jenkins pipeline file 2025-05-03 13:28:30 +03:00
dzaitsev 6ac1fdb14b Merge remote-tracking branch 'origin/dev' into dev 2025-05-02 22:47:03 +03:00
dzaitsev c2b02b8e69 Latest check 2025-05-02 22:46:56 +03:00
Alex Bezdieniezhnykh 9ef30ea661 fix config 2025-05-02 19:26:21 +03:00
Alex Bezdieniezhnykh b429445512 test commit for build 2025-05-02 18:08:16 +03:00
Alex Bezdieniezhnykh 269ba43d7a Merge remote-tracking branch 'origin/dev' into dev 2025-05-02 17:45:42 +03:00
Alex Bezdieniezhnykh 24442869c0 fix queue update 2025-05-02 17:45:30 +03:00
dzaitsev 04c7b124ed Merge remote-tracking branch 'origin/dev' into dev 2025-05-02 16:11:49 +03:00
dzaitsev 9b50c17133 Test polling 2025-05-02 16:11:41 +03:00
Alex Bezdieniezhnykh 472ed6533e fix hardware service 2025-05-02 13:25:33 +03:00
Alex Bezdieniezhnykh 7842fe1067 Merge remote-tracking branch 'origin/dev' into dev 2025-05-02 09:41:18 +03:00
Alex Bezdieniezhnykh 59eb39d447 switch from hardware object to string
and replace mac address to disk serial number
for more predictable hash key
2025-05-02 09:41:04 +03:00
dzaitsev 0bf0cb94f9 Testing autobuild in Jenkins!!!! 2025-05-01 02:34:36 +03:00
dzaitsev b0b1a7daeb Testing autobuild in Jenkins! 2025-05-01 01:45:54 +03:00
dzaitsev db44ef02f1 Testing autobuild in Jenkins111222 2025-05-01 01:45:23 +03:00
dzaitsev 071856d2db Testing autobuild in Jenkins111 2025-05-01 01:37:24 +03:00
dzaitsev 506b2ba6de Testing autobuild in Jenkins 2025-05-01 01:31:23 +03:00
dzaitsev 1b4901d568 Merge remote-tracking branch 'origin/dev' into dev 2025-05-01 01:18:24 +03:00
dzaitsev a1aedd7332 testing Polling in Jenkins ...to be removed later. 2025-05-01 01:18:05 +03:00
Alex Bezdieniezhnykh 28069f63f9 Reapply "import Tensorrt not in compile time in order to dynamically load tensorrt only if nvidia gpu is present"
This reverts commit cf01e5d952.
2025-04-30 23:47:46 +03:00
Alex Bezdieniezhnykh cf01e5d952 Revert "import Tensorrt not in compile time in order to dynamically load tensorrt only if nvidia gpu is present"
This reverts commit 1c4bdabfb5.
2025-04-30 23:32:03 +03:00
Alex Bezdieniezhnykh 1c4bdabfb5 import Tensorrt not in compile time in order to dynamically load tensorrt only if nvidia gpu is present 2025-04-30 23:08:53 +03:00
Alex Bezdieniezhnykh ae83bc8542 fix checking nvidia gpu 2025-04-30 22:19:42 +03:00
Alex Bezdieniezhnykh b3db108f59 add missing packages to build script 2025-04-30 21:43:36 +03:00
Alex Bezdieniezhnykh 3299192f86 fix build after vibe coding 2025-04-30 21:05:37 +03:00
Alex Bezdieniezhnykh 2d83aa06b9 fix build script 2025-04-30 20:57:15 +03:00
Alex Bezdieniezhnykh ce22620e58 update build script 2025-04-30 20:14:23 +03:00
Alex Bezdieniezhnykh a1ee077e0a split to 2 files: tensorrt_engine and onnx engine 2025-04-30 19:33:59 +03:00
Deen 4bca61d859 Update build.cmd
updated collection of libs/submodules for cv2
2025-04-30 12:52:50 +03:00
Deen 42a0c37913 Update requirements.txt
onnx added
2025-04-30 12:50:31 +03:00
Deen 0f9352186e Update requirements.txt
specified opencv-python==4.10.0.84
2025-04-30 12:17:54 +03:00
Deen 47a925f5af Update build.cmd
no-cache-dir for pip install
2025-04-30 12:17:19 +03:00
Alex Bezdieniezhnykh 99a723cfe6 fix publish.cmd 2025-04-30 10:32:40 +03:00
Alex Bezdieniezhnykh e3db942e6b refine build scripts 2025-04-30 10:23:17 +03:00
Alex Bezdieniezhnykh be8e8bdc56 refine build scripts 2025-04-30 10:23:06 +03:00
Alex Bezdieniezhnykh 4e2d3c7e4f update publish.cmd 2025-04-30 00:31:29 +03:00
Alex Bezdieniezhnykh 4d1f72f6f9 update publish.cmd 2025-04-30 00:13:33 +03:00
Alex Bezdieniezhnykh 28d8cc7fcc check existsing in publish.cmd 2025-04-28 18:38:35 +03:00
Alex Bezdieniezhnykh 046cc0eac6 add build cdn_manager in publish.cmd 2025-04-28 14:49:04 +03:00
Alex Bezdieniezhnykh 0274dd65a4 add jenkins pipeline
update publish.cmd
2025-04-28 13:45:51 +03:00
Alex Bezdieniezhnykh babcbc0fc7 clean postbuild script
clean warnings
2025-04-28 10:20:06 +03:00
Alex Bezdieniezhnykh 47aa8b862b add silent detection - don't send to queue if enable 2025-04-27 21:43:45 +03:00
Alex Bezdieniezhnykh 5ff4ee58b9 fix publish script 2025-04-24 22:07:34 +03:00
Alex Bezdieniezhnykh 4ae25ceecf bump version 2025-04-24 16:54:13 +03:00
Alex Bezdieniezhnykh 77151b54e9 fix build scripts 2025-04-24 16:53:39 +03:00
Alex Bezdieniezhnykh e9a44e368d autoconvert tensor rt engine from onnx to specific CUDA gpu 2025-04-24 16:30:21 +03:00
Alex Bezdieniezhnykh e798af470b read cdn yaml config from api
automate tensorrt model conversion in case of no existing one for user's gpu
2025-04-23 23:20:08 +03:00
Alex Bezdieniezhnykh c68c293448 update validation logic 2025-04-21 17:02:13 +03:00
Alex Bezdieniezhnykh 70148bdfdf fix config and installer 2025-04-19 07:20:10 +03:00
Alex Bezdieniezhnykh d42409de7d fix throttle ext
fix configs
fix build scripts
2025-04-17 19:40:09 +03:00
Alex Bezdieniezhnykh 277aaf09b0 throttle reimplemented 2025-04-17 09:16:34 +03:00
Alex Bezdieniezhnykh 0c66607ed7 failsafe load dlls
add user config queue offsets
throttle improvements
2025-04-17 01:19:48 +03:00
Alex Bezdieniezhnykh 0237e279a5 don't send user dto back 2025-04-17 00:18:40 +03:00
Alex Bezdieniezhnykh 1287c13516 fix ui bugs, fix RefreshThumbnails method 2025-04-14 19:43:14 +03:00
Alex Bezdieniezhnykh dd42292eee script fixes 2025-04-14 17:25:54 +03:00
Alex Bezdieniezhnykh 73f0e10ca8 Merge branch 'main' into dev 2025-04-14 17:15:42 +03:00
Alex Bezdieniezhnykh 6109dc18f2 script fixes 2025-04-14 14:03:28 +03:00
Alex Bezdieniezhnykh d0bceae0dc update deploy scripts 2025-04-14 10:31:50 +03:00
Alex Bezdieniezhnykh 80de2ad4d0 add gps matcher service 2025-04-14 10:20:01 +03:00
Alex Bezdieniezhnykh ca1682a86e add gps matcher service 2025-04-14 09:50:34 +03:00
Alex Bezdieniezhnykh 36b3bf1712 put better caption in result image 2025-04-03 11:56:01 +03:00
Alex Bezdieniezhnykh c9800107a6 add generating result image to Results directory 2025-04-03 11:06:00 +03:00
Alex Bezdieniezhnykh 83ae6a0ae9 move detection classes and other system values from local config to remote
forbid non validators to read from queue
create better visualization in detector control
make colors for detection classes more distinguishable
fix bug with removing detection (probably completely)
2025-04-02 19:53:03 +03:00
Alex Bezdieniezhnykh e182547dc8 add model big files to gitignore 2025-04-02 00:30:51 +03:00
Alex Bezdieniezhnykh b21f8e320f fix bug with annotation result gradient stops
add tensorrt engine
2025-04-02 00:29:21 +03:00
Alex Bezdieniezhnykh e0c88bd8fb fix close external clients 2025-03-24 11:48:26 +02:00
Alex Bezdieniezhnykh 73c2ab5374 stop inference on stop pressed
small fixes
2025-03-24 10:52:32 +02:00
Alex Bezdieniezhnykh 6429ad62c2 refactor external clients
put model batch size as parameter in config
2025-03-24 00:33:41 +02:00
Alex Bezdieniezhnykh 32f9de3c71 fix security 2025-03-20 14:50:34 +02:00
Alex Bezdieniezhnykh 099f9cf52b add map support for gps denied 2025-03-17 09:08:43 +02:00
Alex Bezdieniezhnykh 33070b90bf fix image save, ui small fixes 2025-03-12 01:07:52 +02:00
Alex Bezdieniezhnykh 06f527e6c3 replace get hardware info to powershell instead of wmic 2025-03-09 11:57:45 +02:00
Alex Bezdieniezhnykh f26b5ac8a2 update installer 2025-03-09 11:22:11 +02:00
Alex Bezdieniezhnykh 4e6624ee58 fix queue, fix ai email and role set 2025-03-06 19:06:09 +02:00
Alex Bezdieniezhnykh dc44340f67 add production configs, update publish script 2025-03-05 20:58:58 +02:00
Alex Bezdieniezhnykh ee94d2f5db add cdn_manager.py and build script 2025-03-04 13:46:49 +02:00
Alex Bezdieniezhnykh aa3b66071f add installer to gitignore 2025-03-04 13:45:23 +02:00
Alex Bezdieniezhnykh 792abce8c4 small ux fixes 2025-03-04 13:30:13 +02:00
Alex Bezdieniezhnykh a493606f64 correct app close
fix publishing
2025-03-03 19:37:07 +02:00
Alex Bezdieniezhnykh f108cca5f5 make installer and upload it to cdn as part of the publish process
remove login pass
2025-03-03 01:22:16 +02:00
Alex Bezdieniezhnykh d93da15528 fix switcher between modes in DatasetExplorer.xaml 2025-03-02 21:32:31 +02:00
Alex Bezdieniezhnykh 227d01ba5e use cbc encryption decryption - works nice with c# 2025-02-28 00:49:40 +02:00
Alex Bezdieniezhnykh 58839933fc fix id problems with day/winter switch 2025-02-26 22:09:07 +02:00
Alex Bezdieniezhnykh d1af7958f8 fix initialization, throttle operations
day/winter/night switcher fixes
2025-02-19 23:07:16 +02:00
Alex Bezdieniezhnykh c314268d1e added better logging to python
add day / winter / night switcher
2025-02-17 18:41:18 +02:00
Alex Bezdieniezhnykh 2ecbc9bfd4 move zmq port to config file for C# and python 2025-02-16 16:35:52 +02:00
Alex Bezdieniezhnykh 0d6ea4264f add publish script, check its work 2025-02-14 23:08:50 +02:00
Alex Bezdieniezhnykh 961d2499de fix inference
fix small issues
2025-02-14 09:00:04 +02:00
Alex Bezdieniezhnykh cfd5483a18 make python app load a bit eariler, making startup a bit faster 2025-02-13 18:13:15 +02:00
Alex Bezdieniezhnykh e329e5bb67 make start faster 2025-02-12 13:49:01 +02:00
Alex Bezdieniezhnykh 43cae0d03c make cython app exit correctly 2025-02-11 20:40:49 +02:00
Alex Bezdieniezhnykh 9973a16ada print detection results 2025-02-10 18:02:44 +02:00
Alex Bezdieniezhnykh 0f13ba384e complete requirements.txt list
fix build.cmd
2025-02-10 16:39:44 +02:00
Alex Bezdieniezhnykh c1b5b5fee2 use nms in the model itself, simplify and make postprocess faster.
make inference in batches, fix c# handling, add overlap handling
2025-02-10 14:55:00 +02:00
Alex Bezdieniezhnykh ba3e3b4a55 move python inference to Azaion.Inference folder 2025-02-06 10:48:03 +02:00
Alex Bezdieniezhnykh 739759628a fixed inference bugs
add DONE during inference, correct handling on C# side
2025-02-01 02:09:11 +02:00
Alex Bezdieniezhnykh e7afa96a0b fix inference UI and annotation saving 2025-01-30 12:33:24 +02:00
Alex Bezdieniezhnykh 62623b7123 add ramdisk, load AI model to ramdisk and start recognition from it
rewrite zmq to DEALER and ROUTER
add GET_USER command to get CurrentUser from Python
all auth is on the python side
inference run and validate annotations on python
2025-01-29 17:45:26 +02:00
Alex Bezdieniezhnykh 82b3b526a7 rewrite to zmq push and pull patterns.
file load works, suite can start up
2025-01-23 14:37:13 +02:00
Alex Bezdieniezhnykh ce25ef38b0 fix auth, decryption, api interaction 2025-01-20 10:17:35 +02:00
Alex Bezdieniezhnykh e21dd7e70f add pxd headers for correct work
fixes definitions
can run until API call
2025-01-16 17:56:58 +02:00
Alex Bezdieniezhnykh 7439005ed7 gitignore python compiled and debug files 2025-01-15 17:00:05 +02:00
Alex Bezdieniezhnykh fb11622c32 rewrite inference and file loading to cython
Step 1: can compile
2025-01-15 16:43:56 +02:00
Alex Bezdieniezhnykh 1bc1d81fde small fixes, renames 2025-01-15 16:41:42 +02:00
Alex Bezdieniezhnykh ae2c62350a remove fix, todo: test 2025-01-03 18:32:56 +02:00
Alex Bezdieniezhnykh 9aebfd787b update icon 2024-12-31 15:49:38 +02:00
Alex Bezdieniezhnykh 8b94837f18 add offset
fixes
add visual validation border and validate functionality
2024-12-28 15:51:27 +02:00
Alex Bezdieniezhnykh 5fe46cd6f5 fix datasetexplorer view
save annotation with detections
fix sending to queue
2024-12-26 23:47:03 +02:00
Alex Bezdieniezhnykh 81dcb2a92e add resource folder to config for proper folder auto upload 2024-12-24 12:55:36 +02:00
Oleksandr Bezdieniezhnykh f5bad95cd5 small refactor 2024-07-31 18:09:28 +03:00
Oleksandr Bezdieniezhnykh 7807f5bc90 add quartz for jobs
configure auto-scan folder and create hls files job WIP
2024-07-26 14:11:29 +03:00
Oleksandr Bezdieniezhnykh 5e55210eac Merge remote-tracking branch 'origin/main'
# Conflicts:
#	Web/Azaion.Web/Azaion.WebService/Program.cs
2024-07-22 16:02:28 +03:00
Oleksandr Bezdieniezhnykh 6f78b88007 add controller for video 2024-07-22 16:01:28 +03:00
Oleksandr Bezdieniezhnykh cb1751ea4e add serilog log 2024-07-22 12:24:55 +03:00
Oleksandr Bezdieniezhnykh bfbfdf6658 add repository with mysql and entities 2024-07-16 14:18:55 +03:00
Alex Bezdieniezhnykh 32c92fedf2 move Windows app to Windows folder, create folder for Web, create simplest web api service 2024-07-11 19:40:17 +03:00
161 changed files with 7190 additions and 1835 deletions
+8
View File
@@ -6,3 +6,11 @@ obj
*.user *.user
log*.txt log*.txt
secured-config secured-config
build
venv
*.c
*.pyd
cython_debug*
dist
AzaionSuiteInstaller.exe
azaion\.*\.big
+120 -27
View File
@@ -3,10 +3,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls" xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF"
xmlns:controls="clr-namespace:Azaion.Annotator.Controls"
xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common" xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:local="clr-namespace:Azaion.Annotator"
Title="Azaion Annotator" Height="800" Width="1100" Title="Azaion Annotator" Height="800" Width="1100"
WindowState="Maximized" WindowState="Maximized"
> >
@@ -48,15 +50,19 @@
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<local:GradientStyleSelector x:Key="GradientStyleSelector"/>
</Window.Resources> </Window.Resources>
<Grid Name="GlobalGrid" <Grid Name="GlobalGrid"
ShowGridLines="False" ShowGridLines="False"
Background="Black"> Background="Black">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*" Name="DetectionSection" />
<RowDefinition Height="28"></RowDefinition> <RowDefinition Height="0" Name="GpsSplitterRow" />
<RowDefinition Height="32"></RowDefinition> <RowDefinition Height="0" Name="GpsSectionRow"/>
<RowDefinition Height="28" Name="ProgressBarSection"/>
<RowDefinition Height="32" Name="ButtonsSection"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid <Grid
@@ -148,14 +154,22 @@
Grid.Column="0" Grid.Column="0"
Name="LvFiles" Name="LvFiles"
Background="Black" Background="Black"
SelectedItem="{Binding Path=SelectedVideo}" Foreground="#FFA4AFCC" SelectedItem="{Binding Path=SelectedVideo}"
Foreground="#FFDDDDDD"
> >
<ListView.Resources> <ListView.Resources>
<Style TargetType="{x:Type ListViewItem}"> <Style TargetType="{x:Type ListViewItem}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding HasAnnotations}" Value="true"> <DataTrigger Binding="{Binding HasAnnotations}" Value="true">
<Setter Property="Background" Value="Gray"/> <Setter Property="Background" Value="#FF505050"/>
</DataTrigger> </DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value=" DimGray" />
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="DimGray"></Setter>
</Trigger>
</Style.Triggers> </Style.Triggers>
<EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter> <EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter>
</Style> </Style>
@@ -221,7 +235,6 @@
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="4" Grid.RowSpan="4"
Background="Black" Background="Black"
RowBackground="#252525"
Foreground="White" Foreground="White"
RowHeaderWidth="0" RowHeaderWidth="0"
Padding="2 0 0 0" Padding="2 0 0 0"
@@ -230,7 +243,8 @@
CellStyle="{DynamicResource DataGridCellStyle1}" CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
CanUserResizeColumns="False"> CanUserResizeColumns="False"
RowStyleSelector="{StaticResource GradientStyleSelector}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Width="60" Width="60"
@@ -253,28 +267,30 @@
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"></Setter>
</Style> </Style>
</DataGridTextColumn.HeaderStyle> </DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0 " EndPoint="1 0">
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor0}" />
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor1}" />
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor2}" />
<GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Grid> </Grid>
<GridSplitter
Name="GpsSplitter"
Background="DarkGray"
ResizeDirection="Rows"
Grid.Row="1"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="Collapsed" />
<controls:MapMatcher
x:Name="MapMatcherComponent"
Grid.Column="0"
Grid.Row="2"
/>
<controls2:UpdatableProgressBar x:Name="VideoSlider" <controls2:UpdatableProgressBar x:Name="VideoSlider"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="3"
Background="#252525" Background="#252525"
Foreground="LightBlue"> Foreground="LightBlue">
</controls2:UpdatableProgressBar> </controls2:UpdatableProgressBar>
@@ -282,7 +298,7 @@
<!-- Buttons --> <!-- Buttons -->
<Grid <Grid
Name="Buttons" Name="Buttons"
Grid.Row="2" Grid.Row="4"
Background="Black" Background="Black"
> >
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -297,7 +313,10 @@
<ColumnDefinition Width="28" /> <!-- 8 --> <ColumnDefinition Width="28" /> <!-- 8 -->
<ColumnDefinition Width="56" /> <!-- 9 --> <ColumnDefinition Width="56" /> <!-- 9 -->
<ColumnDefinition Width="28" /> <!-- 10 --> <ColumnDefinition Width="28" /> <!-- 10 -->
<ColumnDefinition Width="*" /> <!-- 11 --> <ColumnDefinition Width="28" /> <!-- 11 -->
<ColumnDefinition Width="28" /> <!-- 12 -->
<ColumnDefinition Width="0" /> <!-- 13 -->
<ColumnDefinition Width="*" /> <!-- 14-->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black" <Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
Click="PlayClick"> Click="PlayClick">
@@ -480,7 +499,7 @@
Grid.Column="10" Grid.Column="10"
Padding="2" Width="25" Padding="2" Width="25"
Height="25" Height="25"
ToolTip="Розпізнати за допомогою AI. Клавіша: [A]" Background="Black" BorderBrush="Black" ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="AutoDetect"> Click="AutoDetect">
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477 <Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4 39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
@@ -508,8 +527,82 @@
21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053 21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053
8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" /> 8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" />
</Button> </Button>
<Button Grid.Column="11" Padding="2" Width="25" Height="25" ToolTip="Показати GPS. Клавіша: [M]" Background="Black" BorderBrush="Black"
Click="SwitchGpsPanel">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V520 H580 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="M307.1,311.97c-12.55-14-22.75-31.86-32.9-47.68c-10.23-15.94-19.78-32.43-27.3-49.83c-7.03-16.28-12.48-33.08-9.25-50.97
c2.87-15.93,11.75-31.29,23.84-42.03c22.3-19.8,57.81-22.55,82.67-5.98c29.17,19.45,39.48,55.06,27.59,87.55
c-6.8,18.59-16.41,36.14-27.02,52.8C332.76,274.63,320.84,294.45,307.1,311.97z M307.01,143.45c-38.65-0.46-39.68,59.79-0.95,60.47
c16.47,0.29,30.83-13.34,31-29.9C337.22,157.75,323.24,143.65,307.01,143.45z" />
<GeometryDrawing Brush="LightGray" Geometry="M367.34,310.68c10.09,2.5,23.61,4.83,31.46,12.19c11.05,10.35-5.42,18.17-14.21,21.43c-24.55,9.11-53.52,10.41-79.44,10.11
c-25.7-0.3-54.62-1.23-78.68-11.19c-7.68-3.18-21.53-10.2-12.52-19.47c8.26-8.49,23.33-11.42,34.5-12.94
c-5.15,1.98-16.18,5.12-17.07,11.49c-1,7.13,9.78,10.81,15.02,12.59c18.28,6.22,38.72,7.58,57.89,7.73
c18.91,0.15,38.85-0.72,57.13-5.92c5.72-1.63,18.65-4.74,20.7-11.49c2.28-7.47-9.8-11.66-15.04-13.71
C367.18,311.22,367.26,310.95,367.34,310.68z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<Button Grid.Column="12"
Padding="2"
Width="25"
Height="25"
ToolTip="Показати обєкти по аудіоаналізу. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="SoundDetections">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Geometry="m19.05,171.43v152.38">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m95.24,95.24v342.86">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m171.43,209.52v76.19">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m247.62,133.33v259.58">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<StatusBar Grid.Column="11" <GeometryDrawing Geometry="m323.81,19.05v457.14">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m401.43,86.69v342.86">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m473.43,209.02v76.19">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<StatusBar Grid.Column="14"
Background="#252525" Background="#252525"
Foreground="White"> Foreground="White">
<StatusBar.ItemsPanel> <StatusBar.ItemsPanel>
+294 -344
View File
@@ -6,21 +6,20 @@ using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
using IntervalTree; using IntervalTree;
using LinqToDB;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MediaPlayer = LibVLCSharp.Shared.MediaPlayer; using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
@@ -38,37 +37,45 @@ public partial class Annotator
private readonly IConfigUpdater _configUpdater; private readonly IConfigUpdater _configUpdater;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<Annotator> _logger; private readonly ILogger<Annotator> _logger;
private readonly VLCFrameExtractor _vlcFrameExtractor;
private readonly IAIDetector _aiDetector;
private readonly AnnotationService _annotationService; private readonly AnnotationService _annotationService;
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IDbFactory _dbFactory;
private readonly IInferenceService _inferenceService;
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
private bool _gpsPanelVisible = false;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(100); public readonly CancellationTokenSource MainCancellationSource = new();
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(300); public CancellationTokenSource DetectionCancellationSource = new();
public bool FollowAI = false;
public bool IsInferenceNow = false;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
private readonly TimeSpan _thresholdAfter = TimeSpan.FromMilliseconds(150);
private readonly IGpsMatcherService _gpsMatcherService;
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
private ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
private ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public IntervalTree<TimeSpan, List<Detection>> Detections { get; set; } = new(); public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
private AutodetectDialog _autoDetectDialog = new() { Topmost = true };
public Annotator( public Annotator(
IConfigUpdater configUpdater, IConfigUpdater configUpdater,
IOptions<AppConfig> appConfig, IOptions<AppConfig> appConfig,
LibVLC libVLC, MediaPlayer mediaPlayer, LibVLC libVLC,
MediaPlayer mediaPlayer,
IMediator mediator, IMediator mediator,
FormState formState, FormState formState,
HelpWindow helpWindow, HelpWindow helpWindow,
ILogger<Annotator> logger, ILogger<Annotator> logger,
VLCFrameExtractor vlcFrameExtractor, AnnotationService annotationService,
IAIDetector aiDetector, IDbFactory dbFactory,
AnnotationService annotationService) IInferenceService inferenceService,
IGpsMatcherService gpsMatcherService)
{ {
InitializeComponent(); InitializeComponent();
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
_configUpdater = configUpdater; _configUpdater = configUpdater;
_libVLC = libVLC; _libVLC = libVLC;
@@ -77,14 +84,32 @@ public partial class Annotator
_formState = formState; _formState = formState;
_helpWindow = helpWindow; _helpWindow = helpWindow;
_logger = logger; _logger = logger;
_vlcFrameExtractor = vlcFrameExtractor;
_aiDetector = aiDetector;
_annotationService = annotationService; _annotationService = annotationService;
_dbFactory = dbFactory;
_inferenceService = inferenceService;
_gpsMatcherService = gpsMatcherService;
Loaded += OnLoaded; Loaded += OnLoaded;
Closed += OnFormClosed; Closed += OnFormClosed;
Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator; Activated += (_, _) => _formState.ActiveWindow = WindowEnum.Annotator;
TbFolder.TextChanged += async (sender, args) =>
{
if (!Path.Exists(TbFolder.Text))
return;
try
{
_appConfig.DirectoriesConfig.VideosDirectory = TbFolder.Text;
await ReloadFiles();
await SaveUserSettings();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
};
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
MapMatcherComponent.Init(_appConfig, _gpsMatcherService);
} }
private void OnLoaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
@@ -94,16 +119,13 @@ public partial class Annotator
_suspendLayout = true; _suspendLayout = true;
MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.LeftPanelWidth); MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.UIConfig.LeftPanelWidth);
MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth); MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.UIConfig.RightPanelWidth);
_suspendLayout = false; _suspendLayout = false;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
ReloadFiles(); LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
AnnotationClasses = new ObservableCollection<DetectionClass>(_appConfig.AnnotationConfig.AnnotationClasses);
LvClasses.ItemsSource = AnnotationClasses;
LvClasses.SelectedIndex = 0;
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]); BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
@@ -141,7 +163,7 @@ public partial class Annotator
_formState.CurrentVideoSize = new Size(vw, vh); _formState.CurrentVideoSize = new Size(vw, vh);
_formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length); _formState.CurrentVideoLength = TimeSpan.FromMilliseconds(_mediaPlayer.Length);
await Dispatcher.Invoke(async () => await ReloadAnnotations(_cancellationTokenSource.Token)); await Dispatcher.Invoke(async () => await ReloadAnnotations());
if (_formState.CurrentMedia?.MediaType == MediaTypes.Image) if (_formState.CurrentMedia?.MediaType == MediaTypes.Image)
{ {
@@ -151,11 +173,16 @@ public partial class Annotator
} }
}; };
LvFiles.MouseDoubleClick += async (_, _) => await _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play)); LvFiles.MouseDoubleClick += async (_, _) =>
LvClasses.SelectionChanged += (_, _) =>
{ {
var selectedClass = (DetectionClass)LvClasses.SelectedItem; if (IsInferenceNow)
FollowAI = false;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
};
LvClasses.DetectionClassChanged += (_, args) =>
{
var selectedClass = args.DetectionClass;
Editor.CurrentAnnClass = selectedClass; Editor.CurrentAnnClass = selectedClass;
_mediator.Publish(new AnnClassSelectedEvent(selectedClass)); _mediator.Publish(new AnnClassSelectedEvent(selectedClass));
}; };
@@ -179,10 +206,12 @@ public partial class Annotator
DgAnnotations.MouseDoubleClick += (sender, args) => DgAnnotations.MouseDoubleClick += (sender, args) =>
{ {
var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow; var dgRow = ItemsControl.ContainerFromElement((DataGrid)sender, (args.OriginalSource as DependencyObject)!) as DataGridRow;
if (dgRow != null)
OpenAnnotationResult((AnnotationResult)dgRow!.Item); OpenAnnotationResult((AnnotationResult)dgRow!.Item);
}; };
DgAnnotations.KeyUp += (sender, args) => DgAnnotations.KeyUp += async (sender, args) =>
{ {
switch (args.Key) switch (args.Key)
{ {
@@ -196,17 +225,9 @@ public partial class Annotator
return; return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList(); var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
foreach (var annotationResult in res) var annotations = res.Select(x => x.Annotation).ToList();
{
var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image);
var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg");
File.Delete(annotationResult.Image);
File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
File.Delete(thumbnailPath);
_formState.AnnotationResults.Remove(annotationResult);
Detections.Remove(Detections.Query(annotationResult.Time));
}
break; break;
} }
}; };
@@ -217,32 +238,35 @@ public partial class Annotator
public void OpenAnnotationResult(AnnotationResult res) public void OpenAnnotationResult(AnnotationResult res)
{ {
if (IsInferenceNow)
FollowAI = false;
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Time.TotalMilliseconds; _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum; VideoSlider.Value = _mediaPlayer.Position * VideoSlider.Maximum;
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}"; StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
Editor.ClearExpiredAnnotations(res.Time); Editor.ClearExpiredAnnotations(res.Annotation.Time);
}); });
AddAnnotationsToCanvas(res.Time, res.Detections, showImage: true); ShowAnnotations(res.Annotation, showImage: true);
} }
private async Task SaveUserSettings() private Task SaveUserSettings()
{ {
if (_suspendLayout) if (_suspendLayout)
return; return Task.CompletedTask;
_appConfig.AnnotationConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value;
_appConfig.AnnotationConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value;
await ThrottleExt.Throttle(() => ThrottleExt.Throttle(() =>
{ {
_configUpdater.Save(_appConfig); _configUpdater.Save(_appConfig);
return Task.CompletedTask; return Task.CompletedTask;
}, TimeSpan.FromSeconds(5)); }, SaveConfigTaskId, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
} }
private void ShowTimeAnnotations(TimeSpan time) private void ShowTimeAnnotations(TimeSpan time)
@@ -254,134 +278,93 @@ public partial class Annotator
Editor.ClearExpiredAnnotations(time); Editor.ClearExpiredAnnotations(time);
}); });
var annotations = Detections.Query(time).SelectMany(x => x).Select(x => new Detection(_formState.GetTimeName(time), x)); ShowAnnotations(TimedAnnotations.Query(time).FirstOrDefault());
AddAnnotationsToCanvas(time, annotations);
} }
private void AddAnnotationsToCanvas(TimeSpan? time, IEnumerable<Detection> labels, bool showImage = false) private void ShowAnnotations(Annotation? annotation, bool showImage = false)
{ {
if (annotation == null)
return;
Dispatcher.Invoke(async () => Dispatcher.Invoke(async () =>
{ {
var canvasSize = Editor.RenderSize;
var videoSize = _formState.CurrentVideoSize; var videoSize = _formState.CurrentVideoSize;
if (showImage) if (showImage)
{ {
var fName = _formState.GetTimeName(time); if (File.Exists(annotation.ImagePath))
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg");
if (File.Exists(imgPath))
{ {
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.Background = new ImageBrush { ImageSource = await annotation.ImagePath.OpenImage() };
_formState.BackgroundTime = time; _formState.BackgroundTime = annotation.Time;
videoSize = Editor.RenderSize; videoSize = Editor.RenderSize;
} }
} }
foreach (var label in labels) Editor.CreateDetections(annotation.Time, annotation.Detections, _appConfig.AnnotationConfig.DetectionClasses, videoSize);
{
var annClass = _appConfig.AnnotationConfig.AnnotationClasses[label.ClassNumber];
var canvasLabel = new CanvasLabel(label, canvasSize, videoSize, label.Probability);
Editor.CreateAnnotation(annClass, time, canvasLabel);
}
}); });
} }
private async Task ReloadAnnotations(CancellationToken ct = default) private async Task ReloadAnnotations()
{ {
_formState.AnnotationResults.Clear(); _formState.AnnotationResults.Clear();
Detections.Clear(); TimedAnnotations.Clear();
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
var labelDir = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory); var annotations = await _dbFactory.Run(async db =>
if (!labelDir.Exists) await db.Annotations.LoadWith(x => x.Detections)
return; .Where(x => x.OriginalMediaName == _formState.VideoName)
.OrderBy(x => x.Time)
.ToListAsync(token: MainCancellationSource.Token));
var labelFiles = labelDir.GetFiles($"{_formState.VideoName}_??????.txt"); TimedAnnotations.Clear();
foreach (var file in labelFiles) _formState.AnnotationResults.Clear();
await AddAnnotations(Path.GetFileNameWithoutExtension(file.Name), await YoloLabel.ReadFromFile(file.FullName, ct), ct); foreach (var ann in annotations)
{
TimedAnnotations.Add(ann.Time.Subtract(_thresholdBefore), ann.Time.Add(_thresholdAfter), ann);
_formState.AnnotationResults.Add(new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, ann));
}
} }
//Load from yolo label file
public async Task AddAnnotations(string name, List<YoloLabel> annotations, CancellationToken ct = default)
=> await AddAnnotations(name, annotations.Select(x => new Detection(name, x)).ToList(), ct);
//Add manually //Add manually
public async Task AddAnnotations(string name, List<Detection> detections, CancellationToken ct = default) public void AddAnnotation(Annotation annotation)
{ {
var time = Constants.GetTime(name); var time = annotation.Time;
var timeValue = time ?? TimeSpan.FromMinutes(0); var previousAnnotations = TimedAnnotations.Query(time);
var previousAnnotations = Detections.Query(timeValue); TimedAnnotations.Remove(previousAnnotations);
Detections.Remove(previousAnnotations); TimedAnnotations.Add(time.Subtract(_thresholdBefore), time.Add(_thresholdAfter), annotation);
Detections.Add(timeValue.Subtract(_thresholdBefore), timeValue.Add(_thresholdAfter), detections);
var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Time == time); var existingResult = _formState.AnnotationResults.FirstOrDefault(x => x.Annotation.Time == time);
if (existingResult != null) if (existingResult != null)
{
try
{
_formState.AnnotationResults.Remove(existingResult); _formState.AnnotationResults.Remove(existingResult);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
}
var dict = _formState.AnnotationResults var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Time, Index = i }) .Select((x, i) => new { x.Annotation.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index); .ToDictionary(x => x.Time, x => x.Index);
var index = dict.Where(x => x.Key < timeValue) var index = dict.Where(x => x.Key < time)
.OrderBy(x => timeValue - x.Key) .OrderBy(x => time - x.Key)
.Select(x => x.Value + 1) .Select(x => x.Value + 1)
.FirstOrDefault(); .FirstOrDefault();
_formState.AnnotationResults.Insert(index, CreateAnnotationReult(timeValue, detections)); var annRes = new AnnotationResult(_appConfig.AnnotationConfig.DetectionClassesDict, annotation);
await File.WriteAllTextAsync($"{_appConfig.DirectoriesConfig.ResultsDirectory}/{_formState.VideoName}.json", JsonConvert.SerializeObject(_formState.AnnotationResults), ct); _formState.AnnotationResults.Insert(index, annRes);
} }
private AnnotationResult CreateAnnotationReult(TimeSpan timeValue, List<Detection> detections) private async Task ReloadFiles()
{
var annotationResult = new AnnotationResult
{
Time = timeValue,
Image = $"{_formState.GetTimeName(timeValue)}.jpg",
Detections = detections,
};
if (detections.Count <= 0)
return annotationResult;
Color GetAnnotationClass(List<int> detectionClasses, int colorNumber)
{
if (detections.Count == 0)
return (-1).ToColor();
return colorNumber >= detectionClasses.Count
? _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.LastOrDefault()].Color
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses[colorNumber]].Color;
}
var detectionClasses = detections.Select(x => x.ClassNumber).Distinct().ToList();
annotationResult.ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => _appConfig.AnnotationConfig.DetectionClassesDict[x].ShortName))
: _appConfig.AnnotationConfig.DetectionClassesDict[detectionClasses.FirstOrDefault()].Name;
annotationResult.ClassColor0 = GetAnnotationClass(detectionClasses, 0);
annotationResult.ClassColor1 = GetAnnotationClass(detectionClasses, 1);
annotationResult.ClassColor2 = GetAnnotationClass(detectionClasses, 2);
annotationResult.ClassColor3 = GetAnnotationClass(detectionClasses, 3);
return annotationResult;
}
private void ReloadFiles()
{ {
var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory); var dir = new DirectoryInfo(_appConfig.DirectoriesConfig.VideosDirectory);
if (!dir.Exists) if (!dir.Exists)
return; return;
var labelNames = new DirectoryInfo(_appConfig.DirectoriesConfig.LabelsDirectory).GetFiles()
.Select(x =>
{
var name = Path.GetFileNameWithoutExtension(x.Name);
return name.Length > 8
? name[..^7]
: name;
})
.GroupBy(x => x)
.Select(gr => gr.Key)
.ToDictionary(x => x);
var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x => var videoFiles = dir.GetFiles(_appConfig.AnnotationConfig.VideoFormats.ToArray()).Select(x =>
{ {
using var media = new Media(_libVLC, x.FullName); using var media = new Media(_libVLC, x.FullName);
@@ -390,24 +373,33 @@ public partial class Annotator
{ {
Name = x.Name, Name = x.Name,
Path = x.FullName, Path = x.FullName,
MediaType = MediaTypes.Video, MediaType = MediaTypes.Video
HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", ""))
}; };
media.ParsedChanged += (_, _) => fInfo.Duration = TimeSpan.FromMilliseconds(media.Duration); media.ParsedChanged += (_, _) => fInfo.Duration = TimeSpan.FromMilliseconds(media.Duration);
return fInfo; return fInfo;
}).ToList(); }).ToList();
var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray()).Select(x => new MediaFileInfo var imageFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
.Select(x => new MediaFileInfo
{ {
Name = x.Name, Name = x.Name,
Path = x.FullName, Path = x.FullName,
MediaType = MediaTypes.Image, MediaType = MediaTypes.Image
HasAnnotations = labelNames.ContainsKey(Path.GetFileNameWithoutExtension(x.Name).Replace(" ", ""))
}); });
var allFiles = videoFiles.Concat(imageFiles).ToList();
AllMediaFiles = new ObservableCollection<MediaFileInfo>(videoFiles.Concat(imageFiles).ToList()); var allFileNames = allFiles.Select(x => x.FName).ToList();
var labelsDict = await _dbFactory.Run(async db => await db.Annotations
.GroupBy(x => x.Name.Substring(0, x.Name.Length - 7))
.Where(x => allFileNames.Contains(x.Key))
.ToDictionaryAsync(x => x.Key, x => x.Key));
foreach (var mediaFile in allFiles)
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
LvFiles.ItemsSource = AllMediaFiles; LvFiles.ItemsSource = AllMediaFiles;
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
BlinkHelp(AllMediaFiles.Count == 0 BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
@@ -417,6 +409,10 @@ public partial class Annotator
private void OnFormClosed(object? sender, EventArgs e) private void OnFormClosed(object? sender, EventArgs e)
{ {
MainCancellationSource.Cancel();
_inferenceService.StopInference();
DetectionCancellationSource.Cancel();
_mediaPlayer.Stop(); _mediaPlayer.Stop();
_mediaPlayer.Dispose(); _mediaPlayer.Dispose();
_libVLC.Dispose(); _libVLC.Dispose();
@@ -431,22 +427,18 @@ public partial class Annotator
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\""); Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
} }
public void SeekTo(long timeMilliseconds) public void SeekTo(long timeMilliseconds, bool setPause = true)
{ {
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(setPause);
_mediaPlayer.Time = timeMilliseconds; _mediaPlayer.Time = timeMilliseconds;
VideoSlider.Value = _mediaPlayer.Position * 100; VideoSlider.Value = _mediaPlayer.Position * 100;
StatusClock.Text = $"{TimeSpan.FromMilliseconds(_mediaPlayer.Time):mm\\:ss} / {_formState.CurrentVideoLength:mm\\:ss}";
} }
private void SeekTo(TimeSpan time) => private void SeekTo(TimeSpan time) =>
SeekTo((long)time.TotalMilliseconds); SeekTo((long)time.TotalMilliseconds);
// private void AddClassBtnClick(object sender, RoutedEventArgs e)
// {
// LvClasses.IsReadOnly = false;
// DetectionClasses.Add(new DetectionClass(DetectionClasses.Count));
// LvClasses.SelectedIndex = DetectionClasses.Count - 1;
// }
private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder(); private async void OpenFolderItemClick(object sender, RoutedEventArgs e) => await OpenFolder();
private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder(); private async void OpenFolderButtonClick(object sender, RoutedEventArgs e) => await OpenFolder();
@@ -458,41 +450,42 @@ public partial class Annotator
IsFolderPicker = true, IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
}; };
if (dlg.ShowDialog() != CommonFileDialogResult.Ok) var dialogResult = dlg.ShowDialog();
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
return; return;
if (!string.IsNullOrEmpty(dlg.FileName))
{
_appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName; _appConfig.DirectoriesConfig.VideosDirectory = dlg.FileName;
await SaveUserSettings(); TbFolder.Text = dlg.FileName;
} await Task.CompletedTask;
ReloadFiles();
} }
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
{ {
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList()); FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
LvFiles.ItemsSource = FilteredMediaFiles;
} }
private void PlayClick(object sender, RoutedEventArgs e) private void PlayClick(object sender, RoutedEventArgs e)
{ {
_mediator.Publish(new PlaybackControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); if (IsInferenceNow)
FollowAI = false;
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
} }
private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Pause)); private void PauseClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Pause));
private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Stop)); private void StopClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Stop));
private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.PreviousFrame)); private void PreviousFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.PreviousFrame));
private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.NextFrame)); private void NextFrameClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.NextFrame));
private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.SaveAnnotations)); private void SaveAnnotationsClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.SaveAnnotations));
private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveSelectedAnns)); private void RemoveSelectedClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveSelectedAnns));
private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.RemoveAllAnns)); private void RemoveAllClick(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.RemoveAllAnns));
private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOffVolume)); private void TurnOffVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOffVolume));
private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.TurnOnVolume)); private void TurnOnVolume(object sender, RoutedEventArgs e) => _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.TurnOnVolume));
private void OpenHelpWindowClick(object sender, RoutedEventArgs e) private void OpenHelpWindowClick(object sender, RoutedEventArgs e)
{ {
@@ -508,215 +501,106 @@ public partial class Annotator
LvFilesContextMenu.DataContext = listItem!.DataContext; LvFilesContextMenu.DataContext = listItem!.DataContext;
} }
private (TimeSpan Time, List<Detection> Detections)? _previousDetection; public void AutoDetect(object sender, RoutedEventArgs e)
public async void AutoDetect(object sender, RoutedEventArgs e)
{ {
if (IsInferenceNow)
{
FollowAI = true;
return;
}
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
return; return;
if (LvFiles.SelectedIndex == -1) if (LvFiles.SelectedIndex == -1)
LvFiles.SelectedIndex = 0; LvFiles.SelectedIndex = 0;
await _mediator.Publish(new PlaybackControlEvent(PlaybackControlEnum.Play)); Dispatcher.Invoke(() => Editor.ResetBackground());
_mediaPlayer.SetPause(true);
var manualCancellationSource = new CancellationTokenSource();
var token = manualCancellationSource.Token;
_autoDetectDialog = new AutodetectDialog
{
Topmost = true,
Owner = this
};
_autoDetectDialog.Closing += (_, _) =>
{
manualCancellationSource.Cancel();
_mediaPlayer.SeekTo(TimeSpan.Zero);
Editor.RemoveAllAnns();
};
_autoDetectDialog.Top = Height - _autoDetectDialog.Height - 80;
_autoDetectDialog.Left = 5;
_autoDetectDialog.Log("Ініціалізація AI...");
IsInferenceNow = true;
FollowAI = true;
DetectionCancellationSource = new CancellationTokenSource();
var detectToken = DetectionCancellationSource.Token;
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
var mediaInfo = Dispatcher.Invoke(() => (MediaFileInfo)LvFiles.SelectedItem); while (!detectToken.IsCancellationRequested)
while (mediaInfo != null)
{ {
_formState.CurrentMedia = mediaInfo; var files = new List<string>();
await Dispatcher.Invoke(async () => await ReloadAnnotations(token)); await Dispatcher.Invoke(async () =>
if (mediaInfo.MediaType == MediaTypes.Image)
{ {
await DetectImage(mediaInfo, manualCancellationSource, token); //Take all medias
await Task.Delay(70, token); files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
//.Where(x => !x.HasAnnotations)
.Take(Constants.DETECTION_BATCH_SIZE)
.Select(x => x.Path)
.ToList() ?? [];
if (files.Count != 0)
{
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
await ReloadAnnotations();
} }
else });
await DetectVideo(mediaInfo, manualCancellationSource, token); if (files.Count == 0)
break;
mediaInfo = Dispatcher.Invoke(() => await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken);
Dispatcher.Invoke(() =>
{ {
if (LvFiles.SelectedIndex == LvFiles.Items.Count - 1) if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
return null; DetectionCancellationSource.Cancel();
LvFiles.SelectedIndex += 1; LvFiles.SelectedIndex += files.Count;
return (MediaFileInfo)LvFiles.SelectedItem;
}); });
} }
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
_autoDetectDialog.Close();
_mediaPlayer.Stop();
LvFiles.Items.Refresh(); LvFiles.Items.Refresh();
IsInferenceNow = false;
FollowAI = false;
}); });
}, token);
_autoDetectDialog.ShowDialog();
Dispatcher.Invoke(() => Editor.ResetBackground());
}
private async Task DetectImage(MediaFileInfo mediaInfo, CancellationTokenSource manualCancellationSource, CancellationToken token)
{
try
{
var fName = Path.GetFileNameWithoutExtension(mediaInfo.Path);
var stream = new FileStream(mediaInfo.Path, FileMode.Open);
var detections = await _aiDetector.Detect(fName, stream, token);
await ProcessDetection((TimeSpan.FromMilliseconds(0), stream), Path.GetExtension(mediaInfo.Path), detections, token);
if (detections.Count != 0)
mediaInfo.HasAnnotations = true;
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
await manualCancellationSource.CancelAsync();
}
}
private async Task DetectVideo(MediaFileInfo mediaInfo, CancellationTokenSource manualCancellationSource, CancellationToken token)
{
var prevSeekTime = 0.0;
await foreach (var timeframe in _vlcFrameExtractor.ExtractFrames(mediaInfo.Path, token))
{
Console.WriteLine($"Detect time: {timeframe.Time}");
try
{
var fName = _formState.GetTimeName(timeframe.Time);
var detections = await _aiDetector.Detect(fName, timeframe.Stream, token);
var isValid = IsValidDetection(timeframe.Time, detections);
if (timeframe.Time.TotalSeconds > prevSeekTime + 1)
{
Dispatcher.Invoke(() => SeekTo(timeframe.Time));
prevSeekTime = timeframe.Time.TotalSeconds;
if (!isValid) //Show frame anyway
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
timeframe.Stream.Seek(0, SeekOrigin.Begin);
bitmap.StreamSource = timeframe.Stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
Dispatcher.Invoke(() =>
{
Editor.RemoveAllAnns();
Editor.Background = new ImageBrush { ImageSource = bitmap };
}); });
} }
}
if (!isValid) private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
continue;
mediaInfo.HasAnnotations = true;
await ProcessDetection(timeframe, "jpg", detections, token);
}
catch (Exception ex)
{ {
_logger.LogError(ex, ex.Message);
await manualCancellationSource.CancelAsync();
}
}
}
private bool IsValidDetection(TimeSpan time, List<Detection> detections)
{
// No AI detection, forbid
if (detections.Count == 0)
return false;
// Very first detection, allow
if (!_previousDetection.HasValue)
return true;
var prev = _previousDetection.Value;
// Time between detections is >= than Frame Recognition Seconds, allow
if (time >= prev.Time.Add(TimeSpan.FromSeconds(_appConfig.AIRecognitionConfig.FrameRecognitionSeconds)))
return true;
// Detection is earlier than previous + FrameRecognitionSeconds.
// Look to the detections more in detail
// More detected objects, allow
if (detections.Count > prev.Detections.Count)
return true;
foreach (var det in detections)
{
var point = new Point(det.CenterX, det.CenterY);
var closestObject = prev.Detections
.Select(p => new
{
Point = p,
Distance = point.SqrDistance(new Point(p.CenterX, p.CenterY))
})
.OrderBy(x => x.Distance)
.First();
// Closest object is farther than Tracking distance confidence, hence it's a different object, allow
if (closestObject.Distance > _appConfig.AIRecognitionConfig.TrackingDistanceConfidence)
return true;
// Since closest object within distance confidence, then it is tracking of the same object. Then if recognition probability for the object > increase from previous
if (det.Probability >= closestObject.Point.Probability + _appConfig.AIRecognitionConfig.TrackingProbabilityIncrease)
return true;
}
return false;
}
private async Task ProcessDetection((TimeSpan Time, Stream Stream) timeframe, string imageExtension, List<Detection> detections, CancellationToken token = default)
{
_previousDetection = (timeframe.Time, detections);
await Dispatcher.Invoke(async () => await Dispatcher.Invoke(async () =>
{ {
try try
{ {
var time = timeframe.Time; var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
{
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
.Select((info, i) => new
{
MediaInfo = info,
Index = i
})
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
if (nextFile != null)
{
LvFiles.SelectedIndex = nextFile.Index;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
}
}
var fName = _formState.GetTimeName(timeframe.Time); AddAnnotation(annotation);
var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.{imageExtension}");
Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; if (FollowAI)
Editor.RemoveAllAnns(); SeekTo(annotationImage.Milliseconds, false);
AddAnnotationsToCanvas(time, detections, true);
await AddAnnotations(fName, detections, token);
await _annotationService.SaveAnnotation(fName, imageExtension, detections, SourceEnum.AI, timeframe.Stream, token); var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
var log = string.Join(Environment.NewLine, detections.Select(det =>
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " + $"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " + $"size=({det.Width:F2}, {det.Height:F2}), " +
$"prob: {det.Probability:F1}%")); $"conf: {det.Confidence*100:F0}%"));
Dispatcher.Invoke(() => _autoDetectDialog.Log(log));
Dispatcher.Invoke(() =>
{
if (_formState.CurrentMedia != null)
_formState.CurrentMedia.HasAnnotations = true;
LvFiles.Items.Refresh();
StatusHelp.Text = log;
});
} }
catch (Exception e) catch (Exception e)
{ {
@@ -724,4 +608,70 @@ public partial class Annotator
} }
}); });
} }
private void SwitchGpsPanel(object sender, RoutedEventArgs e)
{
_gpsPanelVisible = !_gpsPanelVisible;
if (_gpsPanelVisible)
{
GpsSplitterRow.Height = new GridLength(4);
GpsSplitter.Visibility = Visibility.Visible;
GpsSectionRow.Height = new GridLength(1, GridUnitType.Star);
MapMatcherComponent.Visibility = Visibility.Visible;
}
else
{
GpsSplitterRow.Height = new GridLength(0);
GpsSplitter.Visibility = Visibility.Collapsed;
GpsSectionRow.Height = new GridLength(0);
MapMatcherComponent.Visibility = Visibility.Collapsed;
}
}
private void SoundDetections(object sender, RoutedEventArgs e)
{
throw new NotImplementedException();
}
}
public class GradientStyleSelector : StyleSelector
{
public override Style? SelectStyle(object item, DependencyObject container)
{
if (container is not DataGridRow row || row.DataContext is not AnnotationResult result)
return null;
var style = new Style(typeof(DataGridRow));
var brush = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(1, 0)
};
var gradients = new List<GradientStop>();
if (result.Colors.Count == 0)
{
var color = (Color)ColorConverter.ConvertFromString("#40DDDDDD");
gradients = [new GradientStop(color, 0.99)];
}
else
{
var increment = 1.0 / result.Colors.Count;
var currentStop = increment;
foreach (var c in result.Colors)
{
var resultColor = c.Color.ToConfidenceColor(c.Confidence);
brush.GradientStops.Add(new GradientStop(resultColor, currentStop));
currentStop += increment;
}
}
foreach (var gradientStop in gradients)
brush.GradientStops.Add(gradientStop);
style.Setters.Add(new Setter(DataGridRow.BackgroundProperty, brush));
return style;
}
} }
+64 -32
View File
@@ -4,9 +4,10 @@ using System.Windows.Input;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.Events;
using Azaion.Common.DTO.Queue; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -22,12 +23,14 @@ public class AnnotatorEventHandler(
FormState formState, FormState formState,
AnnotationService annotationService, AnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger, ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig) IOptions<DirectoriesConfig> dirConfig,
IInferenceService inferenceService)
: :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>, INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent> INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent>
{ {
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
@@ -51,12 +54,12 @@ public class AnnotatorEventHandler(
await Task.CompletedTask; await Task.CompletedTask;
} }
private void SelectClass(DetectionClass annClass) private void SelectClass(DetectionClass detClass)
{ {
mainWindow.Editor.CurrentAnnClass = annClass; mainWindow.Editor.CurrentAnnClass = detClass;
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected)) foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
ann.DetectionClass = annClass; ann.DetectionClass = detClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id; mainWindow.LvClasses.SelectNum(detClass.Id);
} }
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default) public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default)
@@ -72,12 +75,12 @@ public class AnnotatorEventHandler(
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1; keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue) if (keyNumber.HasValue)
SelectClass((DetectionClass)mainWindow.LvClasses.Items[keyNumber.Value]!); SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, cancellationToken); await ControlPlayback(value, cancellationToken);
if (key == Key.A) if (key == Key.R)
mainWindow.AutoDetect(null!, null!); mainWindow.AutoDetect(null!, null!);
#region Volume #region Volume
@@ -105,7 +108,7 @@ public class AnnotatorEventHandler(
#endregion #endregion
} }
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken = default) public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default)
{ {
await ControlPlayback(notification.PlaybackControl, cancellationToken); await ControlPlayback(notification.PlaybackControl, cancellationToken);
mainWindow.VideoView.Focus(); mainWindow.VideoView.Focus();
@@ -121,12 +124,15 @@ public class AnnotatorEventHandler(
switch (controlEnum) switch (controlEnum)
{ {
case PlaybackControlEnum.Play: case PlaybackControlEnum.Play:
Play(); await Play(cancellationToken);
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
mediaPlayer.Pause(); mediaPlayer.Pause();
if (mainWindow.IsInferenceNow)
mainWindow.FollowAI = false;
if (!mediaPlayer.IsPlaying) if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]); mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue) if (formState.BackgroundTime.HasValue)
{ {
mainWindow.Editor.ResetBackground(); mainWindow.Editor.ResetBackground();
@@ -134,6 +140,8 @@ public class AnnotatorEventHandler(
} }
break; break;
case PlaybackControlEnum.Stop: case PlaybackControlEnum.Stop:
inferenceService.StopInference();
await mainWindow.DetectionCancellationSource.CancelAsync();
mediaPlayer.Stop(); mediaPlayer.Stop();
break; break;
case PlaybackControlEnum.PreviousFrame: case PlaybackControlEnum.PreviousFrame:
@@ -164,10 +172,10 @@ public class AnnotatorEventHandler(
mediaPlayer.Volume = 0; mediaPlayer.Volume = 0;
break; break;
case PlaybackControlEnum.Previous: case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true); await NextMedia(isPrevious: true, ct: cancellationToken);
break; break;
case PlaybackControlEnum.Next: case PlaybackControlEnum.Next:
NextMedia(); await NextMedia(ct: cancellationToken);
break; break;
case PlaybackControlEnum.None: case PlaybackControlEnum.None:
break; break;
@@ -182,7 +190,7 @@ public class AnnotatorEventHandler(
} }
} }
private void NextMedia(bool isPrevious = false) private async Task NextMedia(bool isPrevious = false, CancellationToken ct = default)
{ {
var increment = isPrevious ? -1 : 1; var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count; var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
@@ -190,7 +198,7 @@ public class AnnotatorEventHandler(
return; return;
mainWindow.LvFiles.SelectedIndex += increment; mainWindow.LvFiles.SelectedIndex += increment;
Play(); await Play(ct);
} }
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken)
@@ -205,7 +213,7 @@ public class AnnotatorEventHandler(
mediaPlayer.Volume = volume; mediaPlayer.Volume = volume;
} }
private void Play() private async Task Play(CancellationToken ct = default)
{ {
if (mainWindow.LvFiles.SelectedItem == null) if (mainWindow.LvFiles.SelectedItem == null)
return; return;
@@ -213,10 +221,14 @@ public class AnnotatorEventHandler(
mainWindow.Editor.ResetBackground(); mainWindow.Editor.ResetBackground();
formState.CurrentMedia = mediaInfo; formState.CurrentMedia = mediaInfo;
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop(); mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]); mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
if (formState.CurrentMedia.MediaType == MediaTypes.Image)
mediaPlayer.SetPause(true);
} }
//SAVE: MANUAL //SAVE: MANUAL
@@ -226,24 +238,20 @@ public class AnnotatorEventHandler(
return; return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time); var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time); var originalMediaName = formState.VideoName;
var fName = originalMediaName.ToTimeName(time);
var currentDetections = mainWindow.Editor.CurrentDetections var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize))) .Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList(); .ToList();
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken); formState.CurrentMedia.HasAnnotations = currentDetections.Count != 0;
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
mainWindow.LvFiles.Items.Refresh(); mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns(); mainWindow.Editor.RemoveAllAnns();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var imageExtension = isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path); var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{Constants.JPG_EXT}");
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}");
if (isVideo)
{
if (formState.BackgroundTime.HasValue) if (formState.BackgroundTime.HasValue)
{ {
//no need to save image, it's already there, just remove background //no need to save image, it's already there, just remove background
@@ -259,14 +267,38 @@ public class AnnotatorEventHandler(
{ {
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height); var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight); mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
if (isVideo)
mediaPlayer.Play(); mediaPlayer.Play();
}
}
else else
{ await NextMedia(ct: cancellationToken);
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true);
NextMedia();
} }
await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
var annotation = await annotationService.SaveAnnotation(originalMediaName, time, currentDetections, token: cancellationToken);
if (isVideo)
mainWindow.AddAnnotation(annotation);
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x);
foreach (var ann in notification.Annotations)
{
if (!annResDict.TryGetValue(ann.Name, out var value))
continue;
formState.AnnotationResults.Remove(value);
mainWindow.TimedAnnotations.Remove(ann);
}
if (formState.AnnotationResults.Count == 0)
{
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
if (media != null)
{
media.HasAnnotations = false;
mainWindow.LvFiles.Items.Refresh();
}
}
await Task.CompletedTask;
} }
} }
+4 -2
View File
@@ -8,11 +8,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
<PackageReference Include="libc.translation" Version="7.1.1" /> <PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.9.1" /> <PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
@@ -25,9 +26,10 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" /> <PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" /> <PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+342
View File
@@ -0,0 +1,342 @@
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Effects;
using GMap.NET.WindowsPresentation;
namespace Azaion.Annotator.Controls
{
public class CircleVisual : FrameworkElement
{
public readonly GMapMarker Marker;
public CircleVisual(GMapMarker m, Brush background)
{
ShadowEffect = new DropShadowEffect();
Marker = m;
Marker.ZIndex = 100;
SizeChanged += CircleVisual_SizeChanged;
MouseEnter += CircleVisual_MouseEnter;
MouseLeave += CircleVisual_MouseLeave;
Loaded += OnLoaded;
Text = "?";
StrokeArrow.EndLineCap = PenLineCap.Triangle;
StrokeArrow.LineJoin = PenLineJoin.Round;
RenderTransform = _scale;
Width = Height = 22;
FontSize = Width / 1.55;
Background = background;
Angle = null;
}
void CircleVisual_SizeChanged(object sender, SizeChangedEventArgs e)
{
Marker.Offset = new Point(-e.NewSize.Width / 2, -e.NewSize.Height / 2);
_scale.CenterX = -Marker.Offset.X;
_scale.CenterY = -Marker.Offset.Y;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateVisual(true);
}
readonly ScaleTransform _scale = new ScaleTransform(1, 1);
void CircleVisual_MouseLeave(object sender, MouseEventArgs e)
{
Marker.ZIndex -= 10000;
Cursor = Cursors.Arrow;
Effect = null;
_scale.ScaleY = 1;
_scale.ScaleX = 1;
}
void CircleVisual_MouseEnter(object sender, MouseEventArgs e)
{
Marker.ZIndex += 10000;
Cursor = Cursors.Hand;
Effect = ShadowEffect;
_scale.ScaleY = 1.5;
_scale.ScaleX = 1.5;
}
public DropShadowEffect ShadowEffect;
static readonly Typeface Font = new Typeface(new FontFamily("Arial"),
FontStyles.Normal,
FontWeights.Bold,
FontStretches.Normal);
FormattedText _fText = null!;
private Brush _background = Brushes.Blue;
public Brush Background
{
get
{
return _background;
}
set
{
if (_background != value)
{
_background = value;
IsChanged = true;
}
}
}
private Brush _foreground = Brushes.White;
public Brush Foreground
{
get
{
return _foreground;
}
set
{
if (_foreground != value)
{
_foreground = value;
IsChanged = true;
ForceUpdateText();
}
}
}
private Pen _stroke = new Pen(Brushes.Blue, 2.0);
public Pen Stroke
{
get
{
return _stroke;
}
set
{
if (_stroke != value)
{
_stroke = value;
IsChanged = true;
}
}
}
private Pen _strokeArrow = new Pen(Brushes.Blue, 2.0);
public Pen StrokeArrow
{
get
{
return _strokeArrow;
}
set
{
if (_strokeArrow != value)
{
_strokeArrow = value;
IsChanged = true;
}
}
}
public double FontSize = 16;
private double? _angle = 0;
public double? Angle
{
get => _angle;
set
{
if (!_angle.HasValue || !value.HasValue ||
Angle.HasValue && Math.Abs(_angle.Value - value.Value) > 11)
{
_angle = value;
IsChanged = true;
}
}
}
public bool IsChanged = true;
void ForceUpdateText()
{
_fText = new FormattedText(_text,
CultureInfo.InvariantCulture,
FlowDirection.LeftToRight,
Font,
FontSize,
Foreground, 1.0);
IsChanged = true;
}
string _text = null!;
public string Text
{
get
{
return _text;
}
set
{
if (_text != value)
{
_text = value;
ForceUpdateText();
}
}
}
Visual _child = null!;
public virtual Visual? Child
{
get => _child;
set
{
if (_child == value)
return;
if (_child != null)
{
RemoveLogicalChild(_child);
RemoveVisualChild(_child);
}
if (value != null)
{
AddVisualChild(value);
AddLogicalChild(value);
}
// cache the new child
_child = value!;
InvalidateVisual();
}
}
public bool UpdateVisual(bool forceUpdate)
{
if (forceUpdate || IsChanged)
{
Child = Create();
IsChanged = false;
return true;
}
return false;
}
int _countCreate;
private DrawingVisual Create()
{
_countCreate++;
var square = new DrawingVisualFx();
using var dc = square.RenderOpen();
dc.DrawEllipse(null,
Stroke,
new Point(Width / 2, Height / 2),
Width / 2 + Stroke.Thickness / 2,
Height / 2 + Stroke.Thickness / 2);
if (Angle.HasValue)
{
dc.PushTransform(new RotateTransform(Angle.Value, Width / 2, Height / 2));
{
var polySeg = new PolyLineSegment(new[]
{
new Point(Width * 0.2, Height * 0.3), new Point(Width * 0.8, Height * 0.3)
},
true);
var pathFig = new PathFigure(new Point(Width * 0.5, -Height * 0.22),
new PathSegment[] {polySeg},
true);
var pathGeo = new PathGeometry(new[] {pathFig});
dc.DrawGeometry(Brushes.AliceBlue, StrokeArrow, pathGeo);
}
dc.Pop();
}
dc.DrawEllipse(Background, null, new Point(Width / 2, Height / 2), Width / 2, Height / 2);
dc.DrawText(_fText, new Point(Width / 2 - _fText.Width / 2, Height / 2 - _fText.Height / 2));
return square;
}
#region Necessary Overrides -- Needed by WPF to maintain bookkeeping of our hosted visuals
protected override int VisualChildrenCount
{
get
{
return Child == null ? 0 : 1;
}
}
protected override Visual? GetVisualChild(int index)
{
return Child;
}
#endregion
}
public class DrawingVisualFx : DrawingVisual
{
public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect",
typeof(Effect),
typeof(DrawingVisualFx),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.AffectsRender,
OnEffectChanged));
public new Effect Effect
{
get
{
return (Effect)GetValue(EffectProperty);
}
set
{
SetValue(EffectProperty, value);
}
}
private static void OnEffectChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var drawingVisualFx = o as DrawingVisualFx;
if (drawingVisualFx != null)
{
drawingVisualFx.SetMyProtectedVisualEffect((Effect)e.NewValue);
}
}
private void SetMyProtectedVisualEffect(Effect effect)
{
VisualEffect = effect;
}
}
}
+163
View File
@@ -0,0 +1,163 @@
<UserControl x:Class="Azaion.Annotator.Controls.MapMatcher"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Annotator.Controls"
xmlns:windowsPresentation="clr-namespace:GMap.NET.WindowsPresentation;assembly=GMap.NET.WindowsPresentation"
xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="1200">
<Grid
Name="MatcherGrid"
ShowGridLines="False"
Background="Black"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" /> <!-- 0 list view -->
<ColumnDefinition Width="4"/> <!-- 1 splitter -->
<ColumnDefinition Width="*" /> <!-- 2 ExplorerEditor -->
<ColumnDefinition Width="4"/> <!-- 3 splitter -->
<ColumnDefinition Width="*" /> <!-- 4 Maps Control -->
</Grid.ColumnDefinitions>
<Grid
HorizontalAlignment="Stretch"
Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid
Grid.Row="0"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="32"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
Grid.Row="0"
HorizontalAlignment="Stretch"
Margin="1"
x:Name="TbGpsMapFolder"></TextBox>
<Button
Grid.Row="0"
Grid.Column="1"
Margin="1"
Click="OpenGpsTilesFolderClick">
. . .
</Button>
</Grid>
<Grid
Grid.Row="1"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<!-- <TextBlock -->
<!-- Grid.Column="0" -->
<!-- Text="Lat" -->
<!-- Background="Gray"/> -->
<Button
Grid.Column="0"
Margin="1"
Click="TestGps">
Test
</Button>
<TextBox
Grid.Column="1"
HorizontalAlignment="Stretch"
x:Name="TbLat"
Text="48.2748909"></TextBox>
</Grid>
<Grid
Grid.Row="2"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Text="Lon"
Background="Gray"/>
<TextBox
Grid.Column="1"
HorizontalAlignment="Stretch"
x:Name="TbLon"
Text="37.3834877"></TextBox>
</Grid>
<ListView Grid.Row="3"
Name="GpsFiles"
Background="Black"
SelectedItem="{Binding Path=SelectedVideo}"
Foreground="#FFDDDDDD">
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasAnnotations}" Value="true">
<Setter Property="Background" Value="#FF505050"/>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value=" DimGray" />
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="DimGray"></Setter>
</Trigger>
</Style.Triggers>
<EventSetter Event="ContextMenuOpening" Handler="GpsFilesContextOpening"></EventSetter>
</Style>
</ListView.Resources>
<ListView.ContextMenu>
<ContextMenu Name="GpsFilesContextMenu">
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn Width="Auto"
Header="Файл"
DisplayMemberBinding="{Binding Path=Name}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
<GridSplitter
Background="DarkGray"
ResizeDirection="Columns"
Grid.Column="1"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<controls:CanvasEditor
Grid.Column="2"
x:Name="GpsImageEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" >
</controls:CanvasEditor>
<GridSplitter
Background="DarkGray"
ResizeDirection="Columns"
Grid.Column="3"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<windowsPresentation:GMapControl
Grid.Column="4"
x:Name="SatelliteMap"
Zoom="20" MaxZoom="24" MinZoom="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
MinWidth="400" />
</Grid>
</UserControl>
@@ -0,0 +1,160 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using GMap.NET;
using GMap.NET.MapProviders;
using GMap.NET.WindowsPresentation;
using Microsoft.WindowsAPICodePack.Dialogs;
namespace Azaion.Annotator.Controls;
public partial class MapMatcher : UserControl
{
private AppConfig _appConfig = null!;
List<MediaFileInfo> _allMediaFiles = new();
private Dictionary<int, Annotation> _annotations = new();
private string _currentDir = null!;
private IGpsMatcherService _gpsMatcherService = null!;
public MapMatcher()
{
InitializeComponent();
}
public void Init(AppConfig appConfig, IGpsMatcherService gpsMatcherService)
{
_appConfig = appConfig;
_gpsMatcherService = gpsMatcherService;
GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey;
SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap;
SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501);
SatelliteMap.MultiTouchEnabled = true;
GpsFiles.MouseDoubleClick += async (sender, args) => { await OpenGpsLocation(GpsFiles.SelectedIndex); };
}
private async Task OpenGpsLocation(int gpsFilesIndex)
{
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
var ann = _annotations.GetValueOrDefault(gpsFilesIndex);
if (ann == null)
return;
GpsImageEditor.Background = new ImageBrush
{
ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage()
};
if (ann.Lat != 0 && ann.Lon != 0)
SatelliteMap.Position = new PointLatLng(ann.Lat, ann.Lon);
}
private void GpsFilesContextOpening(object sender, ContextMenuEventArgs e)
{
var listItem = sender as ListViewItem;
GpsFilesContextMenu.DataContext = listItem!.DataContext;
}
private void OpenContainingFolder(object sender, RoutedEventArgs e)
{
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo;
if (mediaFileInfo == null)
return;
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
}
private async void OpenGpsTilesFolderClick(object sender, RoutedEventArgs e)
{
var dlg = new CommonOpenFileDialog
{
Title = "Open Video folder",
IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
};
var dialogResult = dlg.ShowDialog();
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
return;
TbGpsMapFolder.Text = dlg.FileName;
_currentDir = dlg.FileName;
var dir = new DirectoryInfo(dlg.FileName);
var mediaFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
.Select(x => new MediaFileInfo
{
Name = x.Name,
Path = x.FullName,
MediaType = MediaTypes.Image
}).ToList();
_allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
_annotations = mediaFiles.Select((x, i) => (i, new Annotation
{
Name = x.Name,
OriginalMediaName = x.Name
})).ToDictionary(x => x.i, x => x.Item2);
var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res));
}
private Task SetMarker(GpsMatchResult result)
{
Dispatcher.Invoke(() =>
{
var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude));
var ann = _annotations[result.Index];
marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue)
{
Text = ann.Name
};
SatelliteMap.Markers.Add(marker);
ann.Lat = result.Latitude;
ann.Lon = result.Longitude;
SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude);
SatelliteMap.ZoomAndCenterMarkers(null);
});
return Task.CompletedTask;
}
private async Task SetFromCsv(List<MediaFileInfo> mediaFiles)
{
var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH);
var csvDict = csvResults
.Where(x => x.MatchType == "stitched")
.ToDictionary(x => x.Index);
foreach (var ann in _annotations)
{
var csvRes = csvDict.GetValueOrDefault(ann.Key);
if (csvRes == null)
continue;
await SetMarker(csvRes);
}
}
private async void TestGps(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
return;
var initialLat = double.Parse(TbLat.Text);
var initialLon = double.Parse(TbLon.Text);
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res));
}
}
+1 -8
View File
@@ -1,14 +1,7 @@
using System.Windows.Input; using MediatR;
using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
public class PlaybackControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
}
public class VolumeChangedEvent(int volume) : INotification public class VolumeChangedEvent(int volume) : INotification
{ {
public int Volume { get; set; } = volume; public int Volume { get; set; } = volume;
@@ -1,9 +0,0 @@
using System.Windows;
namespace Azaion.Annotator.Extensions;
public static class PointExtensions
{
public static double SqrDistance(this Point p1, Point p2) =>
(p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y);
}
@@ -1,130 +0,0 @@
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Azaion.Common.DTO.Config;
using LibVLCSharp.Shared;
using Microsoft.Extensions.Options;
using SkiaSharp;
namespace Azaion.Annotator.Extensions;
public class VLCFrameExtractor(LibVLC libVLC, IOptions<AIRecognitionConfig> config)
{
private const uint RGBA_BYTES = 4;
private const int PLAYBACK_RATE = 4;
private uint _pitch; // Number of bytes per "line", aligned to x32.
private uint _lines; // Number of lines in the buffer, aligned to x32.
private uint _width; // Thumbnail width
private uint _height; // Thumbnail height
private MediaPlayer _mediaPlayer = null!;
private TimeSpan _lastFrameTimestamp;
private long _lastFrame;
private static uint Align32(uint size)
{
if (size % 32 == 0)
return size;
return (size / 32 + 1) * 32;// Align on the next multiple of 32
}
private static SKBitmap? _currentBitmap;
private static readonly ConcurrentQueue<FrameInfo> FramesQueue = new();
private static long _frameCounter;
public async IAsyncEnumerable<(TimeSpan Time, Stream Stream)> ExtractFrames(string mediaPath,
[EnumeratorCancellation] CancellationToken manualCancellationToken = default)
{
var videoFinishedCancellationSource = new CancellationTokenSource();
_mediaPlayer = new MediaPlayer(libVLC);
_mediaPlayer.Stopped += (s, e) => videoFinishedCancellationSource.CancelAfter(1);
using var media = new Media(libVLC, mediaPath);
await media.Parse(cancellationToken: videoFinishedCancellationSource.Token);
var videoTrack = media.Tracks.FirstOrDefault(x => x.Data.Video.Width != 0);
_width = videoTrack.Data.Video.Width;
_height = videoTrack.Data.Video.Height;
_pitch = Align32(_width * RGBA_BYTES);
_lines = Align32(_height);
_mediaPlayer.SetRate(PLAYBACK_RATE);
media.AddOption(":no-audio");
_mediaPlayer.SetVideoFormat("RV32", _width, _height, _pitch);
_mediaPlayer.SetVideoCallbacks(Lock, null, Display);
_mediaPlayer.Play(media);
_frameCounter = 0;
var surface = SKSurface.Create(new SKImageInfo((int) _width, (int) _height));
var videoFinishedCT = videoFinishedCancellationSource.Token;
while ( !(FramesQueue.IsEmpty && videoFinishedCT.IsCancellationRequested || manualCancellationToken.IsCancellationRequested))
{
if (FramesQueue.TryDequeue(out var frameInfo))
{
if (frameInfo.Bitmap == null)
continue;
surface.Canvas.DrawBitmap(frameInfo.Bitmap, 0, 0); // Effectively crops the original bitmap to get only the visible area
using var outputImage = surface.Snapshot();
using var data = outputImage.Encode(SKEncodedImageFormat.Jpeg, 85);
using var ms = new MemoryStream();
data.SaveTo(ms);
yield return (frameInfo.Time, ms);
frameInfo.Bitmap?.Dispose();
}
else
{
await Task.Delay(TimeSpan.FromSeconds(1), videoFinishedCT);
}
}
FramesQueue.Clear(); //clear queue in case of manual stop
_mediaPlayer.Stop();
_mediaPlayer.Dispose();
}
private IntPtr Lock(IntPtr opaque, IntPtr planes)
{
_currentBitmap = new SKBitmap(new SKImageInfo((int)(_pitch / RGBA_BYTES), (int)_lines, SKColorType.Bgra8888));
Marshal.WriteIntPtr(planes, _currentBitmap.GetPixels());
return IntPtr.Zero;
}
private void Display(IntPtr opaque, IntPtr picture)
{
var playerTime = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
if (_lastFrameTimestamp != playerTime)
{
_lastFrame = _frameCounter;
_lastFrameTimestamp = playerTime;
}
if (_frameCounter > 20 && _frameCounter % config.Value.FramePeriodRecognition == 0)
{
var msToAdd = (_frameCounter - _lastFrame) * (_lastFrame == 0 ? 0 : _lastFrameTimestamp.TotalMilliseconds / _lastFrame);
var time = _lastFrameTimestamp.Add(TimeSpan.FromMilliseconds(msToAdd));
FramesQueue.Enqueue(new FrameInfo(time, _currentBitmap));
}
else
{
_currentBitmap?.Dispose();
}
_currentBitmap = null;
_frameCounter++;
}
}
public class FrameInfo(TimeSpan time, SKBitmap? bitmap)
{
public TimeSpan Time { get; set; } = time;
public SKBitmap? Bitmap { get; set; } = bitmap;
}
-89
View File
@@ -1,89 +0,0 @@
using System.IO;
using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Services;
using Azaion.CommonSecurity.Services;
using Compunet.YoloV8;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Detection = Azaion.Common.DTO.Detection;
namespace Azaion.Annotator;
public interface IAIDetector
{
Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken = default);
}
public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable
{
private readonly AIRecognitionConfig _recognitionConfig = recognitionConfig.Value;
private YoloPredictor? _predictor;
private const string YOLO_MODEL = "azaion.onnx";
public async Task<List<Detection>> Detect(string fName, Stream imageStream, CancellationToken cancellationToken)
{
if (_predictor == null)
{
await using var stream = await resourceLoader.Load(YOLO_MODEL, cancellationToken);
_predictor = new YoloPredictor(stream.ToArray());
}
imageStream.Seek(0, SeekOrigin.Begin);
var image = Image.Load<Rgb24>(imageStream);
var result = await _predictor.DetectAsync(image);
var imageSize = new System.Windows.Size(image.Width, image.Height);
var detections = result.Select(d =>
{
var label = new YoloLabel(new CanvasLabel(d.Name.Id, d.Bounds.X, d.Bounds.Y, d.Bounds.Width, d.Bounds.Height), imageSize, imageSize);
return new Detection(fName, label, (double?)d.Confidence * 100);
}).ToList();
return FilterOverlapping(detections);
}
private List<Detection> FilterOverlapping(List<Detection> detections)
{
var k = _recognitionConfig.TrackingIntersectionThreshold;
var filteredDetections = new List<Detection>();
for (var i = 0; i < detections.Count; i++)
{
var detectionSelected = false;
for (var j = i + 1; j < detections.Count; j++)
{
var intersect = detections[i].ToRectangle();
intersect.Intersect(detections[j].ToRectangle());
var maxArea = Math.Max(detections[i].ToRectangle().Area(), detections[j].ToRectangle().Area());
if (!(intersect.Area() > k * maxArea))
continue;
if (detections[i].Probability > detections[j].Probability)
{
filteredDetections.Add(detections[i]);
detections.RemoveAt(j);
}
else
{
filteredDetections.Add(detections[j]);
detections.RemoveAt(i);
}
detectionSelected = true;
break;
}
if (!detectionSelected)
filteredDetections.Add(detections[i]);
}
return filteredDetections;
}
public void Dispose() => _predictor?.Dispose();
}
+7 -1
View File
@@ -7,15 +7,21 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="linq2db.SQLite" Version="5.4.1" /> <PackageReference Include="linq2db.SQLite" Version="5.4.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MessagePack" Version="3.1.0" /> <PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" /> <PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
<PackageReference Include="System.Drawing.Common" Version="4.7.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<PackageReference Include="System.Drawing.Common" Version="5.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+58 -35
View File
@@ -1,11 +1,15 @@
using System.Windows; using System.Windows;
using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
namespace Azaion.Common; namespace Azaion.Common;
public class Constants public class Constants
{ {
public const string SECURE_RESOURCE_CACHE = "SecureResourceCache"; public const string JPG_EXT = ".jpg";
#region DirectoriesConfig #region DirectoriesConfig
@@ -14,25 +18,39 @@ public class Constants
public const string DEFAULT_IMAGES_DIR = "images"; public const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results"; public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
public const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
#endregion #endregion
#region AnnotatorConfig #region AnnotatorConfig
public static readonly List<DetectionClass> DefaultAnnotationClasses = public static readonly AnnotationConfig DefaultAnnotationConfig = new()
{
DetectionClasses = DefaultAnnotationClasses!,
VideoFormats = DefaultVideoFormats!,
ImageFormats = DefaultImageFormats!,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
private static readonly List<DetectionClass> DefaultAnnotationClasses =
[ [
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() },
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" }, new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() },
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" }, new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() },
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" }, new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() },
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" }, new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() },
new() { Id = 6, Name = "Військовий", ShortName = "Військов" }, new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() },
new() { Id = 7, Name = "Накати", ShortName = "Накати" }, new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() },
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() },
new() { Id = 9, Name = "Дим", ShortName = "Дим" }, new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() },
new() { Id = 10, Name = "Літак", ShortName = "Літак" }, new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() },
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() },
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() }
]; ];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
@@ -47,49 +65,45 @@ public class Constants
# region AIRecognitionConfig # region AIRecognitionConfig
public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
{
FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
};
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15; public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
public const int DETECTION_BATCH_SIZE = 4;
# endregion AIRecognitionConfig # endregion AIRecognitionConfig
#region Thumbnails #region Thumbnails
public static readonly ThumbnailConfig DefaultThumbnailConfig = new()
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
};
public static readonly Size DefaultThumbnailSize = new(240, 135); public static readonly Size DefaultThumbnailSize = new(240, 135);
public const int DEFAULT_THUMBNAIL_BORDER = 10; public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb"; public const string THUMBNAIL_PREFIX = "_thumb";
public const string RESULT_PREFIX = "_result";
#endregion #endregion
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
#region Queue #region Queue
public const string MQ_DIRECT_TYPE = "direct";
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm"; public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
#endregion #endregion
@@ -102,4 +116,13 @@ public class Constants
#endregion #endregion
#region Mode Captions
public const string REGULAR_MODE_CAPTION = "Норма";
public const string WINTER_MODE_CAPTION = "Зима";
public const string NIGHT_MODE_CAPTION = "Ніч";
#endregion
public const string CSV_PATH = "matches.csv";
} }
+39 -36
View File
@@ -34,13 +34,13 @@ public class CanvasEditor : Canvas
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register( DependencyProperty.Register(
nameof(GetTimeFunc), nameof(GetTimeFunc),
typeof(Func<TimeSpan?>), typeof(Func<TimeSpan>),
typeof(CanvasEditor), typeof(CanvasEditor),
new PropertyMetadata(null)); new PropertyMetadata(null));
public Func<TimeSpan?> GetTimeFunc public Func<TimeSpan> GetTimeFunc
{ {
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp); get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value); set => SetValue(GetTimeFuncProp, value);
} }
@@ -54,7 +54,7 @@ public class CanvasEditor : Canvas
_verticalLine.Fill = value.ColorBrush; _verticalLine.Fill = value.ColorBrush;
_horizontalLine.Stroke = value.ColorBrush; _horizontalLine.Stroke = value.ColorBrush;
_horizontalLine.Fill = value.ColorBrush; _horizontalLine.Fill = value.ColorBrush;
_classNameHint.Text = value.Name; _classNameHint.Text = value.ShortName;
_newAnnotationRect.Stroke = value.ColorBrush; _newAnnotationRect.Stroke = value.ColorBrush;
_newAnnotationRect.Fill = value.ColorBrush; _newAnnotationRect.Fill = value.ColorBrush;
@@ -84,7 +84,7 @@ public class CanvasEditor : Canvas
}; };
_classNameHint = new TextBlock _classNameHint = new TextBlock
{ {
Text = CurrentAnnClass?.Name ?? "asd", Text = CurrentAnnClass?.ShortName ?? "",
Foreground = new SolidColorBrush(Colors.Black), Foreground = new SolidColorBrush(Colors.Black),
Cursor = Cursors.Arrow, Cursor = Cursors.Arrow,
FontSize = 16, FontSize = 16,
@@ -154,7 +154,25 @@ public class CanvasEditor : Canvas
private void CanvasMouseUp(object sender, MouseButtonEventArgs e) private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{ {
if (SelectionState == SelectionState.NewAnnCreating) if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this)); {
var endPos = e.GetPosition(this);
_newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0;
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
if (width < MIN_SIZE || height < MIN_SIZE)
return;
var time = GetTimeFunc();
CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
Confidence = 1
});
}
SelectionState = SelectionState.None; SelectionState = SelectionState.None;
e.Handled = true; e.Handled = true;
@@ -291,39 +309,25 @@ public class CanvasEditor : Canvas
SetTop(_newAnnotationRect, currentPos.Y); SetTop(_newAnnotationRect, currentPos.Y);
} }
private void CreateAnnotation(Point endPos) public void CreateDetections(TimeSpan time, IEnumerable<Detection> detections, List<DetectionClass> detectionClasses, Size videoSize)
{ {
_newAnnotationRect.Width = 0; foreach (var detection in detections)
_newAnnotationRect.Height = 0;
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
if (width < MIN_SIZE || height < MIN_SIZE)
return;
var time = GetTimeFunc();
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel
{ {
Width = width, var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
Height = height, var canvasLabel = new CanvasLabel(detection, RenderSize, videoSize, detection.Confidence);
X = Math.Min(endPos.X, _newAnnotationStartPos.X), CreateDetectionControl(detectionClass, time, canvasLabel);
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y) }
});
} }
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel) private void CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
{ {
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
{ detectionControl.MouseDown += AnnotationPositionStart;
Width = canvasLabel.Width, SetLeft(detectionControl, canvasLabel.X );
Height = canvasLabel.Height SetTop(detectionControl, canvasLabel.Y);
}; Children.Add(detectionControl);
annotationControl.MouseDown += AnnotationPositionStart; CurrentDetections.Add(detectionControl);
SetLeft(annotationControl, canvasLabel.X ); _newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
SetTop(annotationControl, canvasLabel.Y);
Children.Add(annotationControl);
CurrentDetections.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl;
} }
#endregion #endregion
@@ -355,8 +359,7 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
var expiredAnns = CurrentDetections.Where(x => var expiredAnns = CurrentDetections.Where(x =>
x.Time.HasValue && Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList(); .ToList();
RemoveAnnotations(expiredAnns); RemoveAnnotations(expiredAnns);
} }
+150 -16
View File
@@ -1,10 +1,44 @@
<DataGrid x:Class="Azaion.Common.Controls.DetectionClasses" <UserControl x:Class="Azaion.Common.Controls.DetectionClasses"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<Style x:Key="ButtonRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}" BorderThickness="1"
Padding="10,5" CornerRadius="2">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Border" Property="Background" Value="Gray"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="DarkGray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</UserControl.Resources>
<Grid Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Your DataGrid with detection classes -->
<DataGrid x:Name="DetectionDataGrid"
Grid.Row="0"
Background="Black" Background="Black"
RowBackground="#252525" RowBackground="#252525"
Foreground="White" Foreground="White"
@@ -15,35 +49,135 @@
CellStyle="{DynamicResource DataGridCellStyle1}" CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
CanUserResizeColumns="False"> CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public"
>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn <DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
Width="50"
Header="Клавіша"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle> <DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"/>
</Style> </Style>
</DataGridTemplateColumn.HeaderStyle> </DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Border Background="{Binding Path=ColorBrush}"> <Border Background="{Binding Path=ColorBrush}">
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock> <TextBlock Text="{Binding Path=ClassNumber}"/>
</Border> </Border>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTextColumn <DataGridTextColumn Width="*" Header="Назва" Binding="{Binding Path=ShortName}" CanUserSort="False">
Width="*"
Header="Назва"
Binding="{Binding Path=Name}"
CanUserSort="False">
<DataGridTextColumn.HeaderStyle> <DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"/>
</Style> </Style>
</DataGridTextColumn.HeaderStyle> </DataGridTextColumn.HeaderStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<!-- StackPanel with mode switcher RadioButtons -->
<StackPanel x:Name="ModeSwitcherPanel"
Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,2,0,2">
<RadioButton x:Name="NormalModeRadioButton"
Tag="0"
GroupName="Mode"
Checked="ModeRadioButton_Checked"
IsChecked="True"
Style="{StaticResource ButtonRadioButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m256,105.5c-83.9,0-152.2,68.3-152.2,152.2 0,83.9 68.3,152.2 152.2,152.2 83.9,0 152.2-68.3
152.2-152.2 0-84-68.3-152.2-152.2-152.2zm0,263.5c-61.4,0-111.4-50-111.4-111.4 0-61.4 50-111.4 111.4-111.4 61.4,0 111.4,50 111.4,111.4
0,61.4-50,111.4-111.4,111.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m256,74.8c11.3,0 20.4-9.1 20.4-20.4v-23c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v23c2.84217e-14,11.3 9.1,20.4 20.4,20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m256,437.2c-11.3,0-20.4,9.1-20.4,20.4v22.9c0,11.3 9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-22.9c0-11.2-9.1-20.4-20.4-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m480.6,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h23c11.3,0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m54.4,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h22.9c11.3,0 20.4-9.1 20.4-20.4 0.1-11.3-9.1-20.4-20.3-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="M400.4,82.8L384.1,99c-8,8-8,20.9,0,28.9s20.9,8,28.9,0l16.2-16.2c8-8,8-20.9,0-28.9S408.3,74.8,400.4,82.8z" />
<GeometryDrawing Brush="LightGray" Geometry="m99,384.1l-16.2,16.2c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l16.2-16.2c8-8 8-20.9 0-28.9s-20.9-7.9-28.9,0z" />
<GeometryDrawing Brush="LightGray" Geometry="m413,384.1c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2z" />
<GeometryDrawing Brush="LightGray" Geometry="m99,127.9c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="RegularModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="EveningModeRadioButton"
Tag="20"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
Style="{StaticResource ButtonRadioButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m444.8,256l50.2-50.2c8-8 8-20.9 0-28.9-8-8-20.9-8-28.9,0l-58.7,58.7h-85c-1.3-4.2-3-8.3-5-12.1l60.1-60.1h83c11.3,
0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-71v-71c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4v83l-60.1,60.1c-3.8-2-7.9-3.7-12.1-5v-85l58.7-58.7c8-8 8-20.9
0-28.9-8-8-20.9-8-28.9,0l-50.3,50.1-50.2-50.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l58.7,58.7v85c-4.2,1.3-8.3,
3-12.1,5l-60.1-60.1v-83c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v71h-71c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,
20.4h83l60.1,60.1c-2,3.8-3.7,7.9-5,12.1h-85l-58.7-58.7c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l50.1,50.3-50.2,50.2c-8,8-8,20.9 0,28.9
8,8 20.9,8 28.9,0l58.7-58.7h85c1.3,4.2 3,8.3 5,12.1l-60.1,60.1h-83c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h71v71c0,11.3
9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-83l60.1-60.1c3.8,2 7.9,3.7 12.1,5v85l-58.7,58.7c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l50.2-50.2
50.2,50.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-58.7-58.7v-85c4.2-1.3 8.3-3 12.1-5l60.1,60.1v83c0,11.3 9.1,20.4 20.4,20.4s20.4-9.1 20.4-20.4v-71h71c11.3,0
20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-83l-60.1-60.1c2-3.8 3.7-7.9 5-12.1h85l58.7,58.7c8,8 20.9,8 28.9,0 8-8 8-20.9
0-28.9l-50-50.2zm-217.3,0c0-15.7 12.8-28.5 28.5-28.5s28.5,12.8 28.5,28.5-12.8,28.5-28.5,28.5-28.5-12.8-28.5-28.5z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="WinterModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="NightModeRadioButton"
Tag="40"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
Style="{StaticResource ButtonRadioButtonStyle}"
>
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m500,113.1c-2.4-7.5-8.9-13-16.8-14.1l-55.2-7.9-24.6-48.9c-3.5-7-10.7-11.4-18.5-11.4-7.8,0-15,4.4-18.5,11.4l-24.6,
48.9-55.2,7.9c-7.8,1.1-14.3,6.6-16.8,14.1-2.4,7.5-0.3,15.8 5.4,21.3l39.7,37.9-9.4,53.4c-1.4,7.7 1.8,15.6 8.1,20.2 6.3,4.7 14.7,5.3 21.7,1.7l49.5-25.5
49.5,25.5c3,1.5 6.2,2.3 9.5,2.3 4.3,0 8.6-1.4 12.2-4 6.3-4.6 9.5-12.5 8.1-20.2l-9.4-53.4 39.7-37.9c5.9-5.5 8-13.8 5.6-21.3zm-81.6,36.9c-5,4.8-7.3,
11.7-6.1,18.5l4.1,23.3-22-11.3c-5.9-3-13-3-18.9,0l-22,11.3 4.1-23.3c1.2-6.8-1.1-13.7-6.1-18.5l-16.9-16.2 23.8-3.4c6.7-1 12.5-5.1 15.5-11.2l11-21.9
11,21.9c3,6 8.8,10.2 15.5,11.2l23.8,3.4-16.8,16.2z" />
<GeometryDrawing Brush="LightGray" Geometry="m442,361c-14.9,3.4-30.3,5.1-45.7,5.1-113.8,0-206.4-92.6-206.4-206.3 0-41.8 12.4-82 35.9-116.3
4.8-7 4.8-16.3 0-23.4-4.8-7.1-13.4-10.5-21.8-8.6-54,12.2-103,42.7-138,86-35.4,43.8-55,99.2-55,155.7 0,66.2 25.8,128.4 72.6,175.2 46.8,46.8
109.1,72.6 175.3,72.6 81.9,0 158.4-40.4 204.8-108.1 4.8-7 4.8-16.3 0-23.4-4.8-7-13.4-10.4-21.7-8.5zm-183.1,98.5c-113.8,0-206.4-92.6-206.4-206.3
0-78.2 45.3-149.1 112.8-183.8-11.2,28.6-17,59.1-17, 90.4 0,66.2 25.8,128.4 72.6,175.2 46.7,46.7 108.8,72.5 174.9,72.6-37.3,33.1-85.8,51.9-136.9,51.9z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="NightModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
</StackPanel>
</Grid>
</UserControl>
@@ -1,9 +1,89 @@
namespace Azaion.Common.Controls; using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
namespace Azaion.Common.Controls;
public class DetectionClassChangedEventArgs(DetectionClass detectionClass, int classNumber) : EventArgs
{
public DetectionClass DetectionClass { get; } = detectionClass;
public int ClassNumber { get; } = classNumber;
}
public partial class DetectionClasses public partial class DetectionClasses
{ {
public event EventHandler<DetectionClassChangedEventArgs>? DetectionClassChanged;
private const int CaptionedMinWidth = 230;
ObservableCollection<DetectionClass> _detectionClasses = new();
public DetectionClasses() public DetectionClasses()
{ {
InitializeComponent(); InitializeComponent();
SizeChanged += (sender, args) =>
{
if (args.NewSize.Width < CaptionedMinWidth)
{
RegularModeButton.Text = "";
WinterModeButton.Text = "";
NightModeButton.Text = "";
}
else
{
RegularModeButton.Text = Constants.REGULAR_MODE_CAPTION;
WinterModeButton.Text= Constants.WINTER_MODE_CAPTION;
NightModeButton.Text= Constants.NIGHT_MODE_CAPTION;
}
};
}
public void Init(List<DetectionClass> detectionClasses)
{
foreach (var dClass in detectionClasses)
{
var cl = (DetectionClass)dClass.Clone();
cl.Color = cl.Color.ToConfidenceColor();
_detectionClasses.Add(cl);
}
DetectionDataGrid.ItemsSource = _detectionClasses;
DetectionDataGrid.SelectedIndex = 0;
}
public int CurrentClassNumber { get; private set; } = 0;
public DetectionClass? CurrentDetectionClass { get; set; }
private void DetectionDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
RaiseDetectionClassChanged();
private void ModeRadioButton_Checked(object sender, RoutedEventArgs e) =>
RaiseDetectionClassChanged();
private void RaiseDetectionClassChanged()
{
var detClass = (DetectionClass)DetectionDataGrid.SelectedItem;
if (detClass == null)
return;
var modeAmplifier = 0;
foreach (var child in ModeSwitcherPanel.Children)
if (child is RadioButton { IsChecked: true } rb)
if (int.TryParse(rb.Tag?.ToString(), out int modeIndex))
{
detClass.PhotoMode = (PhotoMode)modeIndex;
modeAmplifier += modeIndex;
}
CurrentDetectionClass = detClass;
CurrentClassNumber = detClass.Id + modeAmplifier;
DetectionClassChanged?.Invoke(this, new DetectionClassChangedEventArgs(detClass, CurrentClassNumber));
}
public void SelectNum(int keyNumber)
{
DetectionDataGrid.SelectedIndex = keyNumber;
} }
} }
+53 -36
View File
@@ -4,6 +4,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.Extensions;
using Label = System.Windows.Controls.Label; using Label = System.Windows.Controls.Label;
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
@@ -11,12 +12,13 @@ namespace Azaion.Common.Controls;
public class DetectionControl : Border public class DetectionControl : Border
{ {
private readonly Action<object, MouseButtonEventArgs> _resizeStart; private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 9; private const double RESIZE_RECT_SIZE = 12;
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly Label _detectionLabel;
private readonly Label _probabilityLabel; public TimeSpan Time { get; set; }
public TimeSpan? Time { get; set; } private readonly double _confidence;
private List<Rectangle> _resizedRectangles = new();
private DetectionClass _detectionClass = null!; private DetectionClass _detectionClass = null!;
public DetectionClass DetectionClass public DetectionClass DetectionClass
@@ -24,9 +26,14 @@ public class DetectionControl : Border
get => _detectionClass; get => _detectionClass;
set set
{ {
_grid.Background = value.ColorBrush; var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
_probabilityLabel.Background = value.ColorBrush; BorderBrush = brush;
_classNameLabel.Text = value.Name; BorderThickness = new Thickness(3);
foreach (var rect in _resizedRectangles)
rect.Stroke = brush;
_detectionLabel.Background = new SolidColorBrush(value.Color.ToConfidenceColor(_confidence));
_detectionLabel.Content = _detectionLabelText(value.UIName);
_detectionClass = value; _detectionClass = value;
} }
} }
@@ -44,28 +51,35 @@ public class DetectionControl : Border
} }
} }
public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null) private string _detectionLabelText(string detectionClassName) =>
_confidence >= 0.995 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%"; //double
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object, MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
{ {
Width = canvasLabel.Width;
Height = canvasLabel.Height;
Time = time; Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
_classNameLabel = new TextBlock _confidence = canvasLabel.Confidence;
var labelContainer = new Canvas
{ {
Text = detectionClass.Name, HorizontalAlignment = HorizontalAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top, VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 15, 0, 0), ClipToBounds = false,
FontSize = 14, Margin = new Thickness(0, 0, 32, 0)
Cursor = Cursors.SizeAll
}; };
_probabilityLabel = new Label _detectionLabel = new Label
{ {
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty, Content = _detectionLabelText(detectionClass.Name),
HorizontalAlignment = HorizontalAlignment.Right, HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top, VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -32, 0, 0), Margin = new Thickness(0, -32, 0, 0),
FontSize = 16, FontSize = 16,
Visibility = Visibility.Visible Visibility = Visibility.Visible
}; };
labelContainer.Children.Add(_detectionLabel);
_selectionFrame = new Rectangle _selectionFrame = new Rectangle
{ {
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
@@ -74,43 +88,46 @@ public class DetectionControl : Border
StrokeThickness = 2, StrokeThickness = 2,
Visibility = Visibility.Collapsed Visibility = Visibility.Collapsed
}; };
_resizedRectangles =
[
CreateResizeRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
CreateResizeRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
CreateResizeRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
CreateResizeRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
CreateResizeRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
CreateResizeRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
CreateResizeRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
CreateResizeRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
];
_grid = new Grid _grid = new Grid
{ {
Background = Brushes.Transparent,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch,
Children = Children = { _selectionFrame }
{
_selectionFrame,
_classNameLabel,
AddRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
AddRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
AddRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
AddRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
AddRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
AddRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
}
}; };
if (probability.HasValue) foreach (var rect in _resizedRectangles)
_grid.Children.Add(_probabilityLabel); _grid.Children.Add(rect);
_grid.Children.Add(labelContainer);
Child = _grid; Child = _grid;
Cursor = Cursors.SizeAll; Cursor = Cursors.SizeAll;
DetectionClass = detectionClass; DetectionClass = detectionClass;
} }
//small corners //small corners
private Rectangle AddRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs) private Rectangle CreateResizeRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
{ {
var rect = new Rectangle() // small rectangles at the corners and sides var rect = new Rectangle() // small rectangles at the corners and sides
{ {
ClipToBounds = false,
Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7),
HorizontalAlignment = ha, HorizontalAlignment = ha,
VerticalAlignment = va, VerticalAlignment = va,
Width = RESIZE_RECT_SIZE, Width = RESIZE_RECT_SIZE,
Height = RESIZE_RECT_SIZE, Height = RESIZE_RECT_SIZE,
Stroke = new SolidColorBrush(Color.FromArgb(230, 40, 40, 40)), // small rectangles color Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
Fill = new SolidColorBrush(Color.FromArgb(1, 255, 255, 255)), Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
Cursor = crs, Cursor = crs,
Name = name, Name = name,
}; };
@@ -120,7 +137,7 @@ public class DetectionControl : Border
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null) public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null)
{ {
var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height); var label = new CanvasLabel(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
return new YoloLabel(label, canvasSize, videoSize); return new YoloLabel(label, canvasSize, videoSize);
} }
} }
-54
View File
@@ -1,54 +0,0 @@
using System.IO;
using System.Windows.Media.Imaging;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO;
public class Annotation
{
private static string _labelsDir = null!;
private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static void InitializeDirs(DirectoriesConfig config)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
}
public string Name { get; set; } = null!;
public string ImageExtension { get; set; } = null!;
public DateTime CreatedDate { get; set; }
public string CreatedEmail { get; set; } = null!;
public RoleEnum CreatedRole { get; set; }
public SourceEnum Source { get; set; }
public AnnotationStatus AnnotationStatus { get; set; }
public IEnumerable<Detection> Detections { get; set; } = null!;
public double Lat { get; set; }
public double Lon { get; set; }
public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
}
public enum AnnotationStatus
{
None = 0,
Created = 10,
Validated = 20
}
public class AnnotationName
{
public string Name { get; set; } = null!;
}
+19 -26
View File
@@ -1,40 +1,33 @@
using System.Windows.Media; using System.Windows.Media;
using Newtonsoft.Json; using Azaion.Common.Database;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationResult public class AnnotationResult
{ {
[JsonProperty(PropertyName = "f")] public Annotation Annotation { get; set; }
public string Image { get; set; } = null!; public List<(Color Color, double Confidence)> Colors { get; private set; }
[JsonProperty(PropertyName = "t")] public string ImagePath { get; set; }
public TimeSpan Time { get; set; } public string TimeStr { get; set; }
public string ClassName { get; set; }
public double Lat { get; set; } public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
public double Lon { get; set; } {
public List<Detection> Detections { get; set; } = new();
#region For XAML Form Annotation = annotation;
[JsonIgnore] TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
public string TimeStr => $"{Time:h\\:mm\\:ss}"; ImagePath = annotation.ImagePath;
[JsonIgnore] var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
public string ClassName { get; set; } = null!;
[JsonIgnore] Colors = annotation.Detections
public Color ClassColor0 { get; set; } .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
.ToList();
[JsonIgnore]
public Color ClassColor1 { get; set; }
[JsonIgnore]
public Color ClassColor2 { get; set; }
[JsonIgnore]
public Color ClassColor3 { get; set; }
#endregion
ClassName = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
: allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
}
} }
@@ -2,11 +2,12 @@
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Common.Database;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
{ {
public Annotation Annotation { get; set; } = annotation; public Annotation Annotation { get; set; } = annotation;
@@ -19,20 +20,21 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage()); Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
return _thumbnail; return _thumbnail;
} }
private set => _thumbnail = value; private set
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public void Delete()
{ {
File.Delete(Annotation.ImagePath); _thumbnail = value;
File.Delete(Annotation.LabelPath); OnPropertyChanged();
File.Delete(Annotation.ThumbPath);
} }
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
public void UpdateUI() => OnPropertyChanged(nameof(IsSeed));
} }
@@ -1,10 +1,19 @@
using MessagePack;
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO.Config;
[MessagePackObject]
public class AIRecognitionConfig public class AIRecognitionConfig
{ {
public double FrameRecognitionSeconds { get; set; } [Key("f_pr")] public int FramePeriodRecognition { get; set; }
public double TrackingDistanceConfidence { get; set; } [Key("f_rs")] public double FrameRecognitionSeconds { get; set; }
public double TrackingProbabilityIncrease { get; set; } [Key("pt")] public double ProbabilityThreshold { get; set; }
public double TrackingIntersectionThreshold { get; set; }
public int FramePeriodRecognition { get; set; } [Key("t_dc")] public double TrackingDistanceConfidence { get; set; }
[Key("t_pi")] public double TrackingProbabilityIncrease { get; set; }
[Key("t_it")] public double TrackingIntersectionThreshold { get; set; }
[Key("d")] public byte[] Data { get; set; } = null!;
[Key("p")] public List<string> Paths { get; set; } = null!;
[Key("m_bs")] public int ModelBatchSize { get; set; } = 2;
} }
+23 -7
View File
@@ -4,20 +4,36 @@ namespace Azaion.Common.DTO.Config;
public class AnnotationConfig public class AnnotationConfig
{ {
public List<DetectionClass> AnnotationClasses { get; set; } = null!; public List<DetectionClass> DetectionClasses { get; set; } = null!;
[JsonIgnore] [JsonIgnore]
private Dictionary<int, DetectionClass>? _detectionClassesDict; private Dictionary<int, DetectionClass>? _detectionClassesDict;
[JsonIgnore]
public Dictionary<int, DetectionClass> DetectionClassesDict => _detectionClassesDict ??= AnnotationClasses.ToDictionary(x => x.Id);
public int? LastSelectedExplorerClass { get; set; } [JsonIgnore]
public Dictionary<int, DetectionClass> DetectionClassesDict
{
get
{
if (_detectionClassesDict != null)
return _detectionClassesDict;
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_detectionClassesDict = DetectionClasses.SelectMany(cls => photoModes.Select(mode => new DetectionClass
{
Id = cls.Id,
Name = cls.Name,
Color = cls.Color,
ShortName = cls.ShortName,
PhotoMode = mode
}))
.ToDictionary(x => x.YoloId, x => x);
return _detectionClassesDict;
}
}
public List<string> VideoFormats { get; set; } = null!; public List<string> VideoFormats { get; set; } = null!;
public List<string> ImageFormats { get; set; } = null!; public List<string> ImageFormats { get; set; } = null!;
public string AnnotationsDbFile { get; set; } = null!; public string AnnotationsDbFile { get; set; } = null!;
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
} }
+25 -30
View File
@@ -8,7 +8,9 @@ namespace Azaion.Common.DTO.Config;
public class AppConfig public class AppConfig
{ {
public ApiConfig ApiConfig { get; set; } = null!; public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public QueueConfig QueueConfig { get; set; } = null!; public QueueConfig QueueConfig { get; set; } = null!;
@@ -16,9 +18,13 @@ public class AppConfig
public AnnotationConfig AnnotationConfig { get; set; } = null!; public AnnotationConfig AnnotationConfig { get; set; } = null!;
public UIConfig UIConfig { get; set; } = null!;
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!; public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public MapConfig MapConfig{ get; set; } = null!;
} }
public interface IConfigUpdater public interface IConfigUpdater
@@ -39,23 +45,13 @@ public class ConfigUpdater : IConfigUpdater
var appConfig = new AppConfig var appConfig = new AppConfig
{ {
ApiConfig = new ApiConfig AnnotationConfig = Constants.DefaultAnnotationConfig,
{
Url = SecurityConstants.DEFAULT_API_URL,
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
},
AnnotationConfig = new AnnotationConfig UIConfig = new UIConfig
{ {
AnnotationClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH, LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH, RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
GenerateAnnotatedImage = false
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
}, },
DirectoriesConfig = new DirectoriesConfig DirectoriesConfig = new DirectoriesConfig
@@ -64,29 +60,28 @@ public class ConfigUpdater : IConfigUpdater
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR, ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR, LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR, ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR,
GpsSatDirectory = Constants.DEFAULT_GPS_SAT_DIRECTORY,
GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY
}, },
ThumbnailConfig = new ThumbnailConfig ThumbnailConfig = Constants.DefaultThumbnailConfig,
{ AIRecognitionConfig = Constants.DefaultAIRecognitionConfig
Size = Constants.DefaultThumbnailSize,
Border = Constants.DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD,
FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION
}
}; };
Save(appConfig); Save(appConfig);
} }
public void Save(AppConfig config) public void Save(AppConfig config)
{ {
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8); //Save only user's config
var publicConfig = new
{
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig
};
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
} }
} }
+7
View File
@@ -0,0 +1,7 @@
namespace Azaion.Common.DTO.Config;
public class MapConfig
{
public string Service { get; set; } = null!;
public string ApiKey { get; set; } = null!;
}
+9
View File
@@ -0,0 +1,9 @@
namespace Azaion.Common.DTO.Config;
public class UIConfig
{
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool GenerateAnnotatedImage { get; set; }
public bool SilentDetection { get; set; }
}
+43 -4
View File
@@ -1,22 +1,61 @@
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class DetectionClass public class DetectionClass : ICloneable
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
public string ShortName { get; set; } = null!; public string ShortName { get; set; } = null!;
[JsonIgnore] public Color Color { get; set; }
public Color Color => Id.ToColor();
[JsonIgnore] [JsonIgnore]
public string UIName
{
get
{
var mode = PhotoMode switch
{
PhotoMode.Night => "(ніч)",
PhotoMode.Winter => "(зим)",
PhotoMode.Regular => "",
_ => ""
};
return ShortName + mode;
}
}
[JsonIgnore]
public PhotoMode PhotoMode { get; set; }
[JsonIgnore] //For UI
public int ClassNumber => Id + 1; public int ClassNumber => Id + 1;
[JsonIgnore]
public int YoloId => Id == -1 ? Id : (int)PhotoMode + Id;
[JsonIgnore] [JsonIgnore]
public SolidColorBrush ColorBrush => new(Color); public SolidColorBrush ColorBrush => new(Color);
public static DetectionClass FromYoloId(int yoloId, List<DetectionClass> detectionClasses)
{
var cls = yoloId % 20;
var photoMode = (PhotoMode)(yoloId - cls);
var detClass = detectionClasses[cls];
detClass.PhotoMode = photoMode;
return detClass;
}
public object Clone() => MemberwiseClone();
}
public enum PhotoMode
{
Regular = 0,
Winter = 20,
Night = 40
} }
+12
View File
@@ -0,0 +1,12 @@
using System.Collections.Concurrent;
namespace Azaion.Common.DTO;
public class DownloadTilesResult
{
public ConcurrentDictionary<(int x, int y), byte[]> Tiles { get; set; } = null!;
public double LatMin { get; set; }
public double LatMax { get; set; }
public double LonMin { get; set; }
public double LonMax { get; set; }
}
+1 -6
View File
@@ -1,5 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Windows; using System.Windows;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
@@ -7,9 +6,7 @@ namespace Azaion.Common.DTO;
public class FormState public class FormState
{ {
public MediaFileInfo? CurrentMedia { get; set; } public MediaFileInfo? CurrentMedia { get; set; }
public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) public string VideoName => CurrentMedia?.FName ?? "";
? ""
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
public string CurrentMrl { get; set; } = null!; public string CurrentMrl { get; set; } = null!;
public Size CurrentVideoSize { get; set; } public Size CurrentVideoSize { get; set; }
@@ -19,6 +16,4 @@ public class FormState
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = [];
public WindowEnum ActiveWindow { get; set; } public WindowEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
} }
+51
View File
@@ -0,0 +1,51 @@
namespace Azaion.Common.DTO;
using System.Collections.Generic;
using System.IO;
public class GpsMatchResult
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public double Latitude { get; set; }
public double Longitude { get; set; }
public int KeyPoints { get; set; }
public int Rotation { get; set; }
public string MatchType { get; set; } = null!;
public static List<GpsMatchResult> ReadFromCsv(string csvFilePath)
{
var imageDatas = new List<GpsMatchResult>();
using var reader = new StreamReader(csvFilePath);
//read header
reader.ReadLine();
if (reader.EndOfStream)
return new List<GpsMatchResult>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line))
continue;
var values = line.Split(',');
if (values.Length == 6)
{
imageDatas.Add(new GpsMatchResult
{
Image = GetFilename(values[0]),
Latitude = double.Parse(values[1]),
Longitude = double.Parse(values[2]),
KeyPoints = int.Parse(values[3]),
Rotation = int.Parse(values[4]),
MatchType = values[5]
});
}
}
return imageDatas;
}
private static string GetFilename(string imagePath) =>
Path.GetFileNameWithoutExtension(imagePath)
.Replace("-small", "");
}
+20 -18
View File
@@ -1,18 +1,18 @@
using System.Drawing; using System.Drawing;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using Size = System.Windows.Size; using Size = System.Windows.Size;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
[MessagePackObject]
public abstract class Label public abstract class Label
{ {
[JsonProperty(PropertyName = "cl")] public int ClassNumber { get; set; } [JsonProperty(PropertyName = "cl")][Key("c")] public int ClassNumber { get; set; }
protected Label() protected Label() { }
{
}
protected Label(int classNumber) protected Label(int classNumber)
{ {
@@ -26,22 +26,22 @@ public class CanvasLabel : Label
public double Y { get; set; } public double Y { get; set; }
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public double? Probability { get; } public double Confidence { get; set; }
public CanvasLabel() public CanvasLabel()
{ {
} }
public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber) public CanvasLabel(int classNumber, double x, double y, double width, double height, double confidence = 1) : base(classNumber)
{ {
X = x; X = x;
Y = y; Y = y;
Width = width; Width = width;
Height = height; Height = height;
Probability = probability; Confidence = confidence;
} }
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double? probability = null) public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double confidence = 1)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
@@ -75,19 +75,20 @@ public class CanvasLabel : Label
Width = label.Width * realWidth; Width = label.Width * realWidth;
Height = label.Height * ch; Height = label.Height * ch;
} }
Probability = probability; Confidence = confidence;
} }
} }
[MessagePackObject]
public class YoloLabel : Label public class YoloLabel : Label
{ {
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; } [JsonProperty(PropertyName = "x")][Key("x")] public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; } [JsonProperty(PropertyName = "y")][Key("y")] public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")] public double Width { get; set; } [JsonProperty(PropertyName = "w")][Key("w")] public double Width { get; set; }
[JsonProperty(PropertyName = "h")] public double Height { get; set; } [JsonProperty(PropertyName = "h")][Key("h")] public double Height { get; set; }
public YoloLabel() public YoloLabel()
{ {
@@ -184,15 +185,16 @@ public class YoloLabel : Label
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
} }
[MessagePackObject]
public class Detection : YoloLabel public class Detection : YoloLabel
{ {
public string AnnotationName { get; set; } [JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
public double? Probability { get; set; } [JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
//For db //For db & serialization
public Detection(){} public Detection(){}
public Detection(string annotationName, YoloLabel label, double? probability = null) public Detection(string annotationName, YoloLabel label, double confidence = 1)
{ {
AnnotationName = annotationName; AnnotationName = annotationName;
ClassNumber = label.ClassNumber; ClassNumber = label.ClassNumber;
@@ -200,6 +202,6 @@ public class Detection : YoloLabel
CenterY = label.CenterY; CenterY = label.CenterY;
Height = label.Height; Height = label.Height;
Width = label.Width; Width = label.Width;
Probability = probability; Confidence = confidence;
} }
} }
+5 -1
View File
@@ -1,4 +1,6 @@
namespace Azaion.Common.DTO; using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class MediaFileInfo public class MediaFileInfo
{ {
@@ -8,4 +10,6 @@ public class MediaFileInfo
public string DurationStr => $"{Duration:h\\:mm\\:ss}"; public string DurationStr => $"{Duration:h\\:mm\\:ss}";
public bool HasAnnotations { get; set; } public bool HasAnnotations { get; set; }
public MediaTypes MediaType { get; set; } public MediaTypes MediaType { get; set; }
public string FName => Name.ToFName();
} }
+2 -1
View File
@@ -15,5 +15,6 @@ public enum PlaybackControlEnum
TurnOnVolume = 10, TurnOnVolume = 10,
Previous = 11, Previous = 11,
Next = 12, Next = 12,
Close = 13 Close = 13,
ValidateAnnotations = 15
} }
@@ -1,4 +1,5 @@
using Azaion.CommonSecurity.DTO; using Azaion.Common.Database;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO.Queue; namespace Azaion.Common.DTO.Queue;
using MessagePack; using MessagePack;
@@ -8,13 +9,15 @@ public class AnnotationCreatedMessage
{ {
[Key(0)] public DateTime CreatedDate { get; set; } [Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!; [Key(1)] public string Name { get; set; } = null!;
[Key(2)] public string ImageExtension { get; set; } = null!; [Key(2)] public string OriginalMediaName { get; set; } = null!;
[Key(3)] public string Detections { get; set; } = null!; [Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public byte[] Image { get; set; } = null!; [Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public RoleEnum CreatedRole { get; set; } [Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public string CreatedEmail { get; set; } = null!; [Key(6)] public byte[] Image { get; set; } = null!;
[Key(7)] public SourceEnum Source { get; set; } [Key(7)] public RoleEnum CreatedRole { get; set; }
[Key(8)] public AnnotationStatus Status { get; set; } [Key(8)] public string CreatedEmail { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; }
[Key(10)] public AnnotationStatus Status { get; set; }
} }
[MessagePackObject] [MessagePackObject]
+31
View File
@@ -0,0 +1,31 @@
using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class SatTile
{
public int X { get; }
public int Y { get; }
public double LeftTopLat { get; }
public double LeftTopLon { get; }
public double BottomRightLat { get; }
public double BottomRightLon { get; }
public string Url { get; set; }
public SatTile(int x, int y, int zoom, string url)
{
X = x;
Y = y;
Url = url;
(LeftTopLat, LeftTopLon) = GeoUtils.TileToWorldPos(x, y, zoom);
(BottomRightLat, BottomRightLon) = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
}
public override string ToString()
{
return $"Tile[X={X}, Y={Y}, TL=({LeftTopLat:F6}, {LeftTopLon:F6}), BR=({BottomRightLat:F6}, {BottomRightLon:F6})]";
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO;
using MessagePack;
namespace Azaion.Common.Database;
[MessagePackObject]
public class Annotation
{
private static string _labelsDir = null!;
private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static void InitializeDirs(DirectoriesConfig config)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
}
[Key("n")] public string Name { get; set; } = null!;
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
[IgnoreMember]public TimeSpan Time { get; set; }
[IgnoreMember]public string ImageExtension { get; set; } = null!;
[IgnoreMember]public DateTime CreatedDate { get; set; }
[IgnoreMember]public string CreatedEmail { get; set; } = null!;
[IgnoreMember]public RoleEnum CreatedRole { get; set; }
[IgnoreMember]public SourceEnum Source { get; set; }
[IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; }
[IgnoreMember]public DateTime ValidateDate { get; set; }
[IgnoreMember]public string ValidateEmail { get; set; } = null!;
[Key("d")] public IEnumerable<Detection> Detections { get; set; } = null!;
[Key("t")] public long Milliseconds { get; set; }
[Key("lat")]public double Lat { get; set; }
[Key("lon")]public double Lon { get; set; }
#region Calculated
[IgnoreMember]public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
[IgnoreMember]public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember]public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember]public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
#endregion Calculated
}
[MessagePackObject]
public class AnnotationImage : Annotation
{
[Key("i")] public byte[] Image { get; set; } = null!;
}
public enum AnnotationStatus
{
None = 0,
Created = 10,
Validated = 20
}
+6
View File
@@ -0,0 +1,6 @@
namespace Azaion.Common.Database;
public class AnnotationName
{
public string Name { get; set; } = null!;
}
+1
View File
@@ -9,4 +9,5 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions
public ITable<Annotation> Annotations => this.GetTable<Annotation>(); public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>(); public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
public ITable<Detection> Detections => this.GetTable<Detection>(); public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
} }
+48 -6
View File
@@ -4,6 +4,7 @@ using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.SQLite; using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -16,6 +17,8 @@ public interface IDbFactory
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func); Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func); Task Run(Func<AnnotationsDb, Task> func);
void SaveToDisk(); void SaveToDisk();
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
@@ -39,8 +42,8 @@ public class DbFactory : IDbFactory
_memoryDataOptions = new DataOptions() _memoryDataOptions = new DataOptions()
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection) .UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); ;//.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr); _fileConnection = new SQLiteConnection(FileConnStr);
@@ -48,7 +51,6 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_fileConnection) .UseConnection(_fileConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
_ = _fileDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
if (!File.Exists(_annConfig.AnnotationsDbFile)) if (!File.Exists(_annConfig.AnnotationsDbFile))
CreateDb(); CreateDb();
@@ -63,6 +65,20 @@ public class DbFactory : IDbFactory
db.CreateTable<Annotation>(); db.CreateTable<Annotation>();
db.CreateTable<AnnotationName>(); db.CreateTable<AnnotationName>();
db.CreateTable<Detection>(); db.CreateTable<Detection>();
db.CreateTable<QueueOffset>();
db.QueueOffsets.BulkCopy(new List<QueueOffset>
{
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_QUEUE
},
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE
}
});
} }
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func) public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
@@ -81,6 +97,23 @@ public class DbFactory : IDbFactory
{ {
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
} }
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
{
var names = annotations.Select(x => x.Name).ToList();
await DeleteAnnotations(names, cancellationToken);
}
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
{
await Run(async db =>
{
var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken);
var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations");
});
SaveToDisk();
}
} }
public static class AnnotationsDbSchemaHolder public static class AnnotationsDbSchemaHolder
@@ -92,10 +125,19 @@ public static class AnnotationsDbSchemaHolder
MappingSchema = new MappingSchema(); MappingSchema = new MappingSchema();
var builder = new FluentMappingBuilder(MappingSchema); var builder = new FluentMappingBuilder(MappingSchema);
builder.Entity<Annotation>() var annotationBuilder = builder.Entity<Annotation>();
.HasTableName(Constants.ANNOTATIONS_TABLENAME) annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name) .HasPrimaryKey(x => x.Name)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName); .Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
annotationBuilder
.Ignore(x => x.Milliseconds)
.Ignore(x => x.Classes)
.Ignore(x => x.Classes)
.Ignore(x => x.ImagePath)
.Ignore(x => x.LabelPath)
.Ignore(x => x.ThumbPath);
builder.Entity<Detection>() builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME); .HasTableName(Constants.DETECTIONS_TABLENAME);
+7
View File
@@ -0,0 +1,7 @@
namespace Azaion.Common.Database;
public class QueueOffset
{
public string QueueName { get; set; } = null!;
public ulong Offset { get; set; }
}
@@ -1,6 +1,7 @@
using MediatR; using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class AnnotationCreatedEvent(Annotation annotation) : INotification public class AnnotationCreatedEvent(Annotation annotation) : INotification
{ {
@@ -0,0 +1,9 @@
using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification
{
public List<Annotation> Annotations { get; set; } = annotations;
}
@@ -0,0 +1,13 @@
using MediatR;
namespace Azaion.Common.DTO;
public class AnnotatorControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
}
public class DatasetExplorerControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
}
@@ -1,7 +1,8 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR; using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{ {
+7 -1
View File
@@ -7,8 +7,14 @@ public static class BitmapExtensions
{ {
public static async Task<BitmapImage> OpenImage(this string imagePath) public static async Task<BitmapImage> OpenImage(this string imagePath)
{ {
var image = new BitmapImage();
await using var stream = File.OpenRead(imagePath); await using var stream = File.OpenRead(imagePath);
return OpenImage(stream);
}
public static BitmapImage OpenImage(this Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
var image = new BitmapImage();
image.BeginInit(); image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad; image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream; image.StreamSource = stream;
+7 -17
View File
@@ -4,25 +4,15 @@ namespace Azaion.Common.Extensions;
public static class ColorExtensions public static class ColorExtensions
{ {
public static Color ToColor(this int id) private const int MIN_ALPHA = 15;
private const int MAX_ALPHA = 150;
public static Color ToConfidenceColor(this Color color, double confidence = 1)
{ {
var index = id % ColorValues.Length; color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA)));
var hex = index == -1
? "#40DDDDDD"
: $"#40{ColorValues[index]}";
var color =(Color)ColorConverter.ConvertFromString(hex);
return color; return color;
} }
private static readonly string[] ColorValues = public static Color ToColor(this string hexColor) =>
[ (Color)ColorConverter.ConvertFromString(hexColor);
"FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000",
"800000", "008000", "000080", "808000", "800080", "008080", "808080",
"C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0",
"400000", "004000", "000040", "404000", "400040", "004040", "404040",
"200000", "002000", "000020", "202000", "200020", "002020", "202020",
"600000", "006000", "000060", "606000", "600060", "006060", "606060",
"A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0",
"E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0"
];
} }
+39
View File
@@ -0,0 +1,39 @@
namespace Azaion.Common.Extensions;
public static class GeoUtils
{
private const double EARTH_RADIUS = 6378137;
public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom)
{
var latRad = lat * Math.PI / 180.0;
var n = Math.Pow(2.0, zoom);
var xTile = (int)Math.Floor((lon + 180.0) / 360.0 * n);
var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
return (xTile, yTile);
}
public static (double lat, double lon) TileToWorldPos(int x, int y, int zoom)
{
var n = Math.Pow(2.0, zoom);
var lonDeg = x / n * 360.0 - 180.0;
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
var latDeg = latRad * 180.0 / Math.PI;
return (latDeg, lonDeg);
}
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(double centerLat, double centerLon, double radiusM)
{
var latRad = centerLat * Math.PI / 180.0;
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
var minLat = Math.Max(centerLat - latDiff, -90.0);
var maxLat = Math.Min(centerLat + latDiff, 90.0);
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
var minLon = Math.Max(centerLon - lonDiff, -180.0);
var maxLon = Math.Min(centerLon + lonDiff, 180.0);
return (minLat, maxLat, minLon, maxLon);
}
}
@@ -0,0 +1,28 @@
using System.Drawing;
namespace Azaion.Common.Extensions;
public static class GraphicsExtensions
{
public static void DrawTextBox(this Graphics g, string text, PointF position, Brush background, Brush foreground)
{
using var textFont = new Font(FontFamily.GenericSerif, 14);
using var stringFormat = new StringFormat();
stringFormat.LineAlignment = StringAlignment.Near;
stringFormat.Alignment = StringAlignment.Center;
var padding = 1.0f;
var textSize = g.MeasureString(text, textFont);
var backgroundRect = new RectangleF(
position.X - textSize.Width / 2.0f - padding,
position.Y - padding,
textSize.Width + 2 * padding,
textSize.Height + 2 * padding
);
g.FillRectangle(background, backgroundRect);
g.DrawString(text, textFont, foreground, position, stringFormat);
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ public class ParallelExt
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
var threadsCount = (int)(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0); var threadsCount = (int)Math.Round(Environment.ProcessorCount * parallelOptions.CpuUtilPercent / 100.0);
var processedCount = 0; var processedCount = 0;
var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount)); var chunkSize = Math.Max(1, (int)(source.Count / (decimal)threadsCount));
@@ -0,0 +1,12 @@
using System.IO;
namespace Azaion.Common.Extensions;
public static class StringExtensions
{
public static string ToFName(this string path) =>
Path.GetFileNameWithoutExtension(path).Replace(" ", "");
public static string ToTimeName(this string fName, TimeSpan? ts) =>
$"{fName}_{ts:hmmssf}";
}
+64 -11
View File
@@ -1,19 +1,72 @@
namespace Azaion.Common.Extensions; using System.Collections.Concurrent;
namespace Azaion.Common.Extensions;
public static class ThrottleExt public static class ThrottleExt
{ {
private static bool _throttleOn; private class ThrottleState(Func<Task> action)
public static async Task Throttle(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
{ {
if (_throttleOn) public Func<Task> Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action));
return; public bool IsCoolingDown = false;
public bool CallScheduledDuringCooldown = false;
public Task CooldownTask = Task.CompletedTask;
public readonly object StateLock = new();
}
_throttleOn = true; private static readonly ConcurrentDictionary<Guid, ThrottleState> ThrottlerStates = new();
await func();
_ = Task.Run(async () => public static void Throttle(Func<Task> action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false)
{ {
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); ArgumentNullException.ThrowIfNull(action);
_throttleOn = false; if (actionId == Guid.Empty)
}, cancellationToken); throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId));
if (interval <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive.");
var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action));
state.Action = action;
lock (state.StateLock)
{
if (!state.IsCoolingDown)
{
state.IsCoolingDown = true;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state);
}
else
{
if (scheduleCallAfterCooldown)
state.CallScheduledDuringCooldown = true;
}
}
}
private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state)
{
try
{
await state.Action();
}
catch (Exception ex)
{
Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}");
}
finally
{
await Task.Delay(interval);
lock (state.StateLock)
{
if (state.CallScheduledDuringCooldown)
{
state.CallScheduledDuringCooldown = false;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state);
}
else
{
state.IsCoolingDown = false;
}
}
}
} }
} }
+123 -44
View File
@@ -5,9 +5,12 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using MediatR; using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -17,115 +20,147 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public class AnnotationService public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
{ {
private readonly AzaionApiClient _apiClient;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer; private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IAzaionApi _api;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!; private Consumer _consumer = null!;
private readonly UIConfig _uiConfig;
private static readonly Guid SaveTaskId = Guid.NewGuid();
public AnnotationService(AzaionApiClient apiClient, public AnnotationService(
IDbFactory dbFactory, IDbFactory dbFactory,
FailsafeAnnotationsProducer producer, FailsafeAnnotationsProducer producer,
IOptions<QueueConfig> queueConfig, IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IGalleryService galleryService, IGalleryService galleryService,
IMediator mediator) IMediator mediator,
IAzaionApi api)
{ {
_apiClient = apiClient;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_producer = producer; _producer = producer;
_galleryService = galleryService; _galleryService = galleryService;
_mediator = mediator; _mediator = mediator;
_api = api;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await Init()).Wait(); Task.Run(async () => await Init()).Wait();
} }
private async Task Init() private async Task Init(CancellationToken cancellationToken = default)
{ {
if (!_api.CurrentUser.Role.IsValidator())
return;
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
{ {
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)}, Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
UserName = _queueConfig.ConsumerUsername, UserName = _queueConfig.ConsumerUsername,
Password = _queueConfig.ConsumerPassword Password = _queueConfig.ConsumerPassword
}); });
var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets();
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
{ {
OffsetSpec = new OffsetTypeFirst(), Reference = _api.CurrentUser.Email,
MessageHandler = async (stream, _, _, message) => OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1),
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)), MessageHandler = async (_, _, context, message) =>
});
}
//AI / Manual
public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
//Queue (only from operators)
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
{ {
if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator) var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents);
offsets.AnnotationsOffset = context.Offset;
ThrottleExt.Throttle(() =>
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
return; return;
await SaveAnnotationInner( await SaveAnnotationInner(
message.CreatedDate, msg.CreatedDate,
message.Name, msg.OriginalMediaName,
message.ImageExtension, msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(message.Detections) ?? [], JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
message.Source, msg.Source,
new MemoryStream(message.Image), new MemoryStream(msg.Image),
message.CreatedRole, msg.CreatedRole,
message.CreatedEmail, msg.CreatedEmail,
cancellationToken); fromQueue: true,
token: cancellationToken);
}
});
} }
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream, //AI
RoleEnum createdRole, 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(),
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,
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
// Manual save from Validators -> Validated -> stream: azaion-annotations-confirm
// AI, Manual save from Operators -> Created -> stream: azaion-annotations
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole,
string createdEmail, string createdEmail,
bool fromQueue = false,
CancellationToken token = default) CancellationToken token = default)
{ {
//Flow for roles:
// Operator:
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
// Validator:
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
AnnotationStatus status; AnnotationStatus status;
var fName = originalMediaName.ToTimeName(time);
var annotation = await _dbFactory.Run(async db => var annotation = await _dbFactory.Run(async db =>
{ {
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator status = userRole.IsValidator() && source == SourceEnum.Manual
? AnnotationStatus.Validated ? AnnotationStatus.Validated
: AnnotationStatus.Created; : AnnotationStatus.Created;
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
if (ann != null) if (ann != null)
{
await db.Annotations await db.Annotations
.Where(x => x.Name == fName) .Where(x => x.Name == fName)
.Set(x => x.Classes, classes)
.Set(x => x.Source, source) .Set(x => x.Source, source)
.Set(x => x.AnnotationStatus, status) .Set(x => x.AnnotationStatus, status)
.Set(x => x.CreatedDate, createdDate)
.Set(x => x.CreatedEmail, createdEmail)
.Set(x => x.CreatedRole, userRole)
.UpdateAsync(token: token); .UpdateAsync(token: token);
ann.Detections = detections;
}
else else
{ {
ann = new Annotation ann = new Annotation
{ {
CreatedDate = createdDate, CreatedDate = createdDate,
Name = fName, Name = fName,
ImageExtension = imageExtension, OriginalMediaName = originalMediaName,
Time = time,
ImageExtension = Constants.JPG_EXT,
CreatedEmail = createdEmail, CreatedEmail = createdEmail,
CreatedRole = createdRole, CreatedRole = userRole,
AnnotationStatus = status, AnnotationStatus = status,
Source = source, Source = source,
Detections = detections Detections = detections
}; };
await db.InsertAsync(ann, token: token); await db.InsertAsync(ann, token: token);
} }
await db.BulkCopyAsync(detections, cancellationToken: token);
return ann; return ann;
}); });
@@ -135,9 +170,53 @@ public class AnnotationService
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
} }
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await _galleryService.CreateThumbnail(annotation, token); await _galleryService.CreateThumbnail(annotation, token);
await _producer.SendToQueue(annotation, token); if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, token);
if (!fromQueue && !_uiConfig.SilentDetection) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
ThrottleExt.Throttle(async () =>
{
_dbFactory.SaveToDisk();
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
return annotation;
}
public async Task ValidateAnnotations(List<Annotation> annotations, CancellationToken token = default)
{
if (!_api.CurrentUser.Role.IsValidator())
return;
var annNames = annotations.Select(x => x.Name).ToHashSet();
await _dbFactory.Run(async db =>
{
await db.Annotations
.Where(x => annNames.Contains(x.Name))
.Set(x => x.AnnotationStatus, AnnotationStatus.Validated)
.Set(x => x.ValidateDate, DateTime.UtcNow)
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
.UpdateAsync(token: token);
});
ThrottleExt.Throttle(async () =>
{
_dbFactory.SaveToDisk();
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken);
foreach (var annotation in notification.Annotations)
{
File.Delete(annotation.ImagePath);
File.Delete(annotation.LabelPath);
File.Delete(annotation.ThumbPath);
}
} }
} }
+32 -9
View File
@@ -1,7 +1,6 @@
using System.IO; using System.IO;
using System.Net; using System.Net;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using LinqToDB; using LinqToDB;
@@ -53,7 +52,7 @@ public class FailsafeAnnotationsProducer
await Init(cancellationToken); await Init(cancellationToken);
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
var messages = await GetFromQueue(cancellationToken); var messages = await GetFromInnerQueue(cancellationToken);
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10 foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
{ {
var sent = false; var sent = false;
@@ -65,58 +64,82 @@ public class FailsafeAnnotationsProducer
.Where(x => x.Status == AnnotationStatus.Created) .Where(x => x.Status == AnnotationStatus.Created)
.Select(x => new Message(MessagePackSerializer.Serialize(x))) .Select(x => new Message(MessagePackSerializer.Serialize(x)))
.ToList(); .ToList();
if (createdMessages.Any())
await _annotationProducer.Send(createdMessages, CompressionType.Gzip); await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
var validatedMessages = messagesChunk var validatedMessages = messagesChunk
.Where(x => x.Status == AnnotationStatus.Validated) .Where(x => x.Status == AnnotationStatus.Validated)
.Select(x => new Message(MessagePackSerializer.Serialize(x))) .Select(x => new Message(MessagePackSerializer.Serialize(x)))
.ToList(); .ToList();
if (validatedMessages.Any())
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip); await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
await _dbFactory.Run(async db => await _dbFactory.Run(async db =>
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
sent = true; sent = true;
_dbFactory.SaveToDisk();
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
} }
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
} }
} }
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
} }
} }
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default) private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default)
{ {
return await _dbFactory.Run(async db => return await _dbFactory.Run(async db =>
{ {
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a) var annotations = await db.AnnotationsQueue.Join(
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a)
.ToListAsync(token: cancellationToken); .ToListAsync(token: cancellationToken);
var messages = new List<AnnotationCreatedMessage>(); var messages = new List<AnnotationCreatedMessage>();
var badImages = new List<string>();
foreach (var annotation in annotations) foreach (var annotation in annotations)
{
try
{ {
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
var annCreateMessage = new AnnotationCreatedMessage var annCreateMessage = new AnnotationCreatedMessage
{ {
Name = annotation.Name, Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time,
CreatedRole = annotation.CreatedRole, CreatedRole = annotation.CreatedRole,
CreatedEmail = annotation.CreatedEmail, CreatedEmail = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate, CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus,
ImageExtension = annotation.ImageExtension,
Image = image, Image = image,
Detections = JsonConvert.SerializeObject(annotation.Detections), Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source Source = annotation.Source,
}; };
messages.Add(annCreateMessage); messages.Add(annCreateMessage);
} }
catch (Exception e)
{
_logger.LogError(e, e.Message);
badImages.Add(annotation.Name);
}
}
if (badImages.Any())
{
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken);
_dbFactory.SaveToDisk();
}
return messages; return messages;
}); });
} }
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default) public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
{ {
await _dbFactory.Run(async db => await _dbFactory.Run(async db =>
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
@@ -0,0 +1,74 @@
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IGpsMatcherService
{
Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default);
void StopGpsMatching();
}
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
{
private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 5;
private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func<GpsMatchResult, Task> processResult, CancellationToken detectToken = default)
{
var currentLat = initialLatitude;
var currentLon = initialLongitude;
var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory);
if (Directory.Exists(routeDir))
Directory.Delete(routeDir, true);
Directory.CreateDirectory(routeDir);
var routeFiles = new List<string>();
foreach (var file in Directory.GetFiles(userRouteDir))
{
routeFiles.Add(file);
File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file)));
}
var indexOffset = 0;
while (routeFiles.Any())
{
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
gpsMatcherClient.StartMatching(new StartMatchingEvent
{
ImagesCount = POINTS_COUNT,
Latitude = initialLatitude,
Longitude = initialLongitude,
SatelliteImagesDir = dirConfig.Value.GpsSatDirectory,
RouteDir = dirConfig.Value.GpsRouteDirectory
});
while (true)
{
var result = gpsMatcherClient.GetResult();
if (result == null)
break;
result.Index += indexOffset;
await processResult(result);
currentLat = result.Latitude;
currentLon = result.Longitude;
routeFiles.RemoveAt(0);
}
indexOffset += POINTS_COUNT;
}
}
public void StopGpsMatching()
{
gpsMatcherClient.Stop();
}
}
+77 -13
View File
@@ -8,6 +8,7 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
@@ -72,10 +73,9 @@ public class GalleryService(
await _updateLock.WaitAsync(); await _updateLock.WaitAsync();
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db => var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name))); await db.Annotations.ToDictionaryAsync(x => x.Name)));
var missedAnnotations = new ConcurrentBag<Annotation>(); var missedAnnotations = new ConcurrentDictionary<string, Annotation>();
try try
{ {
var prefixLen = Constants.THUMBNAIL_PREFIX.Length; var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles() var thumbnails = ThumbnailsDirectory.GetFiles()
@@ -89,7 +89,7 @@ public class GalleryService(
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{ {
var fName = Path.GetFileNameWithoutExtension(file.Name); var fName = file.Name.ToFName();
try try
{ {
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt"); var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
@@ -97,6 +97,7 @@ public class GalleryService(
{ {
File.Delete(file.FullName); File.Delete(file.FullName);
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!"); logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
await dbFactory.DeleteAnnotations([fName], cancellationToken);
return; return;
} }
@@ -105,8 +106,36 @@ public class GalleryService(
return; return;
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList(); var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
//get names and time
var fileName = Path.GetFileNameWithoutExtension(file.Name);
var strings = fileName.Split("_");
var timeStr = strings.LastOrDefault();
string originalMediaName;
TimeSpan time;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!string.IsNullOrEmpty(timeStr) &&
timeStr.Length == 6 &&
int.TryParse(timeStr[..1], out var hours) &&
int.TryParse(timeStr[1..3], out var minutes) &&
int.TryParse(timeStr[3..5], out var seconds) &&
int.TryParse(timeStr[5..], out var milliseconds))
{
time = new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
originalMediaName = fileName[..^7];
}
else
{
originalMediaName = fileName;
time = TimeSpan.FromSeconds(0);
}
var annotation = new Annotation var annotation = new Annotation
{ {
Time = time,
OriginalMediaName = originalMediaName,
Name = fName, Name = fName,
ImageExtension = Path.GetExtension(file.Name), ImageExtension = Path.GetExtension(file.Name),
Detections = detections, Detections = detections,
@@ -117,8 +146,15 @@ public class GalleryService(
AnnotationStatus = AnnotationStatus.Validated AnnotationStatus = AnnotationStatus.Validated
}; };
//Remove duplicates
if (!existingAnnotations.ContainsKey(fName)) if (!existingAnnotations.ContainsKey(fName))
missedAnnotations.Add(annotation); {
if (missedAnnotations.ContainsKey(fName))
Console.WriteLine($"{fName} is already exists! Duplicate!");
else
missedAnnotations.TryAdd(fName, annotation);
}
if (!thumbnails.Contains(fName)) if (!thumbnails.Contains(fName))
await CreateThumbnail(annotation, cancellationToken); await CreateThumbnail(annotation, cancellationToken);
@@ -142,21 +178,30 @@ public class GalleryService(
ProgressUpdateInterval = 200 ProgressUpdateInterval = 200
}); });
} }
catch (Exception e)
{
logger.LogError(e, $"Failed to refresh thumbnails! Error: {e.Message}");
}
finally finally
{ {
var copyOptions = new BulkCopyOptions var copyOptions = new BulkCopyOptions
{ {
MaxBatchSize = 50 MaxBatchSize = 50
}; };
//Db could be updated during the long files scraping
existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name)));
var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList();
var annotationsToInsert = missedAnnotations
.Where(a => !existingAnnotations.ContainsKey(a.Key))
.Select(x => x.Value)
.ToList();
await dbFactory.Run(async db => await dbFactory.Run(async db =>
{ {
var xx = missedAnnotations.GroupBy(x => x.Name) await db.BulkCopyAsync(copyOptions, annotationsToInsert);
.Where(gr => gr.Count() > 1) await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
.ToList();
foreach (var gr in xx)
Console.WriteLine(gr.Key);
await db.BulkCopyAsync(copyOptions, missedAnnotations);
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections));
}); });
dbFactory.SaveToDisk(); dbFactory.SaveToDisk();
_updateLock.Release(); _updateLock.Release();
@@ -227,8 +272,7 @@ public class GalleryService(
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color; var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale)); g.DrawRectangle(new Pen(brush, width: 3), (float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle);
} }
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg); bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
@@ -238,6 +282,25 @@ public class GalleryService(
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
} }
} }
public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token)
{
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
using var g = Graphics.FromImage(originalImage);
foreach (var detection in annotation.Detections)
{
var detClass = _annotationConfig.DetectionClassesDict[detection.ClassNumber];
var color = detClass.Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
g.DrawRectangle(new Pen(brush, width: 3), (float)det.X, (float)det.Y, (float)det.Width, (float)det.Height);
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black);
}
originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg);
}
} }
public interface IGalleryService public interface IGalleryService
@@ -248,4 +311,5 @@ public interface IGalleryService
Task RefreshThumbnails(); Task RefreshThumbnails();
Task ClearThumbnails(CancellationToken cancellationToken = default); Task ClearThumbnails(CancellationToken cancellationToken = default);
Task CreateAnnotatedImage(Annotation annotation, CancellationToken token);
} }
+118
View File
@@ -0,0 +1,118 @@
using System.Diagnostics;
using Azaion.Common.DTO;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IGpsMatcherClient
{
void StartMatching(StartMatchingEvent startEvent);
GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class StartMatchingEvent
{
public string RouteDir { get; set; } = null!;
public string SatelliteImagesDir { get; set; } = null!;
public int ImagesCount { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public string ProcessingType { get; set; } = "cuda";
public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24;
public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
}
public class GpsMatcherClient : IGpsMatcherClient
{
private readonly GpsDeniedClientConfig _gpsDeniedClientConfig;
private readonly RequestSocket _requestSocket = new();
private readonly SubscriberSocket _subscriberSocket = new();
public GpsMatcherClient(IOptions<GpsDeniedClientConfig> gpsDeniedClientConfig)
{
_gpsDeniedClientConfig = gpsDeniedClientConfig.Value;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.ExternalGpsDeniedPath,
WorkingDirectory = SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER
//Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
//process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}");
_subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}");
_subscriberSocket.Subscribe("");
}
public void StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
var response = _requestSocket.ReceiveFrameString();
if (response != "OK")
throw new Exception("Start Matching Failed");
}
public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retries)
{
if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update))
continue;
if (update == "FINISHED")
return null;
var parts = update.Split(',');
if (parts.Length != 5)
throw new Exception("Matching Result Failed");
return new GpsMatchResult
{
Index = int.Parse(parts[0]),
Image = parts[1],
Latitude = double.Parse(parts[2]),
Longitude = double.Parse(parts[3]),
MatchType = parts[4]
};
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each");
return null;
}
public void Stop()
{
_requestSocket.SendFrame("STOP");
}
}
@@ -0,0 +1,57 @@
using System.Text;
using Azaion.Common.Database;
using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.Services;
using MessagePack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IInferenceService
{
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default);
void StopInference();
}
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IAzaionApi azaionApi, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService
{
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default)
{
client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials));
var aiConfig = aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
while (!detectToken.IsCancellationRequested)
{
try
{
var bytes = client.GetBytes(ct: detectToken);
if (bytes == null)
throw new Exception("Can't get bytes from inference client");
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
return;
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken);
await processAnnotation(annotationImage);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
break;
}
}
}
public void StopInference()
{
client.Send(RemoteCommand.Create(CommandType.StopInference));
}
}
@@ -0,0 +1,244 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Azaion.Common.Services;
public interface ISatelliteDownloader
{
Task GetTiles(double latitude, double longitude, double radiusM, int zoomLevel, CancellationToken token = default);
}
public class SatelliteDownloader(
ILogger<SatelliteDownloader> logger,
IOptions<MapConfig> mapConfig,
IOptions<DirectoriesConfig> directoriesConfig,
IHttpClientFactory httpClientFactory)
: ISatelliteDownloader
{
private const int INPUT_TILE_SIZE = 256;
private const string TILE_URL_TEMPLATE = "https://mt{0}.google.com/vt/lyrs=s&x={1}&y={2}&z={3}&token={4}";
private const int NUM_SERVERS = 4;
private const int CROP_WIDTH = 1024;
private const int CROP_HEIGHT = 1024;
private const int STEP_X = 300;
private const int STEP_Y = 300;
private const int OUTPUT_TILE_SIZE = 512;
private readonly string _apiKey = mapConfig.Value.ApiKey;
private readonly string _satDirectory = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, directoriesConfig.Value.GpsSatDirectory);
public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
{
//empty Satellite directory
if (Directory.Exists(_satDirectory))
Directory.Delete(_satDirectory, true);
Directory.CreateDirectory(_satDirectory);
var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token);
var image = ComposeTiles(downloadTilesResult.Tiles, token);
if (image != null)
await SplitToTiles(image, downloadTilesResult, token);
}
private async Task SplitToTiles(Image<Rgba32> image, DownloadTilesResult bounds, CancellationToken token = default)
{
// Calculate all crop parameters beforehand
var cropTasks = new List<Action>();
var latRange = bounds.LatMax - bounds.LatMin; // [cite: 13]
var lonRange = bounds.LonMax - bounds.LonMin; // [cite: 13]
var degreesPerPixelLat = latRange / image.Height; // [cite: 13]
var degreesPerPixelLon = lonRange / image.Width; // [cite: 14]
int tempRowIndex = 0;
for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y) // [cite: 15]
{
int tempColIndex = 0;
for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X) // [cite: 16]
{
// Capture loop variables for the closure
int currentTop = top;
int currentLeft = left;
int rowIndex = tempRowIndex;
int colIndex = tempColIndex;
cropTasks.Add(() =>
{
token.ThrowIfCancellationRequested();
var cropBox = new Rectangle(currentLeft, currentTop, CROP_WIDTH, CROP_HEIGHT);
using var croppedImage = image.Clone(ctx => ctx.Crop(cropBox));
var cropTlLat = bounds.LatMax - (currentTop * degreesPerPixelLat);
var cropTlLon = bounds.LonMin + (currentLeft * degreesPerPixelLon);
var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat);
var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon);
var outputFilename = Path.Combine(_satDirectory,
$"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif"
);
using var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3));
resizedImage.SaveAsTiffAsync(outputFilename, token).GetAwaiter().GetResult(); // Use synchronous saving or manage async Tasks properly in parallel context
});
tempColIndex++;
}
tempRowIndex++;
}
// Execute tasks in parallel
await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token);
}
private Image<Rgba32>? ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default)
{
if (downloadedTiles.IsEmpty)
return null;
var xMin = downloadedTiles.Min(t => t.Key.x);
var xMax = downloadedTiles.Max(t => t.Key.x);
var yMin = downloadedTiles.Min(t => t.Key.y);
var yMax = downloadedTiles.Max(t => t.Key.y);
var totalWidth = (xMax - xMin + 1) * INPUT_TILE_SIZE;
var totalHeight = (yMax - yMin + 1) * INPUT_TILE_SIZE;
if (totalWidth <= 0 || totalHeight <= 0)
return null;
var largeImage = new Image<Rgba32>(totalWidth, totalHeight);
largeImage.Mutate(ctx =>
{
for (var y = yMin; y <= yMax; y++)
{
for (var x = xMin; x <= xMax; x++)
{
if (!downloadedTiles.TryGetValue((x, y), out var tileData))
continue;
try
{
using var tileImage = Image.Load(tileData);
var offsetX = (x - xMin) * INPUT_TILE_SIZE;
var offsetY = (y - yMin) * INPUT_TILE_SIZE;
ctx.DrawImage(tileImage, new Point(offsetX, offsetY), 1f);
}
catch (Exception)
{
Console.WriteLine($"Error while loading tile: {tileData}");
}
if (token.IsCancellationRequested)
return;
}
}
});
return largeImage;
}
private record SessionResponse(string Session);
private async Task<string?> GetSessionToken()
{
var url = $"https://tile.googleapis.com/v1/createSession?key={_apiKey}";
using var httpClient = httpClientFactory.CreateClient();
try
{
var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
var response = await httpClient.PostAsync(url, new StringContent(str));
response.EnsureSuccessStatusCode();
var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>();
return sessionResponse?.Session;
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
private async Task<DownloadTilesResult> DownloadTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default)
{
var (latMin, latMax, lonMin, lonMax) = GeoUtils.GetBoundingBox(centerLat, centerLon, radiusM);
var (xMin, yMin) = GeoUtils.WorldToTilePos(latMax, lonMin, zoomLevel); // Top-left corner
var (xMax, yMax) = GeoUtils.WorldToTilePos(latMin, lonMax, zoomLevel); // Bottom-right corner
var tilesToDownload = new ConcurrentQueue<SatTile>();
var downloadedTiles = new ConcurrentDictionary<(int x, int y), byte[]>();
var server = 0;
var sessionToken = await GetSessionToken();
for (var y = yMin; y <= yMax + 1; y++)
for (var x = xMin; x <= xMax + 1; x++)
{
token.ThrowIfCancellationRequested();
var url = string.Format(TILE_URL_TEMPLATE, server, x, y, zoomLevel, sessionToken);
tilesToDownload.Enqueue(new SatTile(x, y, zoomLevel, url));
server = (server + 1) % NUM_SERVERS;
}
var downloadTasks = new List<Task>();
int downloadedCount = 0;
for (int i = 0; i < NUM_SERVERS; i++)
{
downloadTasks.Add(Task.Run(async () =>
{
using var httpClient = httpClientFactory.CreateClient();
while (tilesToDownload.TryDequeue(out var tileInfo))
{
if (token.IsCancellationRequested) break;
try
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36");
var response = await httpClient.GetAsync(tileInfo.Url, token);
response.EnsureSuccessStatusCode();
var tileData = await response.Content.ReadAsByteArrayAsync(token);
if (tileData?.Length > 0)
{
downloadedTiles.TryAdd((tileInfo.X, tileInfo.Y), tileData);
Interlocked.Increment(ref downloadedCount);
}
}
catch (HttpRequestException requestException)
{
logger.LogError(requestException, $"Fail to download tile! Url: {tileInfo.Url}. {requestException.Message}");
}
catch (Exception e)
{
logger.LogError(e, $"Fail to download tile! {e.Message}");
}
}
}, token));
}
await Task.WhenAll(downloadTasks);
return new DownloadTilesResult
{
Tiles = downloadedTiles,
LatMin = latMin,
LatMax = latMax,
LonMin = lonMin,
LonMax = lonMax
};
}
}
@@ -1,12 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="NetMQ" Version="4.0.1.13" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup> </ItemGroup>
-8
View File
@@ -1,8 +0,0 @@
namespace Azaion.CommonSecurity.DTO;
public class ApiConfig
{
public string Url { get; set; } = null!;
public int RetryCount {get;set;}
public double TimeoutSeconds { get; set; }
}
+7 -1
View File
@@ -1,7 +1,13 @@
namespace Azaion.CommonSecurity.DTO; using MessagePack;
namespace Azaion.CommonSecurity.DTO;
[MessagePackObject]
public class ApiCredentials(string email, string password) : EventArgs public class ApiCredentials(string email, string password) : EventArgs
{ {
[Key(nameof(Email))]
public string Email { get; set; } = email; public string Email { get; set; } = email;
[Key(nameof(Password))]
public string Password { get; set; } = password; public string Password { get; set; } = password;
} }
@@ -0,0 +1,7 @@
namespace Azaion.CommonSecurity.DTO;
internal class BusinessExceptionDto
{
public int ErrorCode { get; set; }
public string Message { get; set; } = string.Empty;
}
@@ -0,0 +1,40 @@
using MessagePack;
namespace Azaion.CommonSecurity.DTO.Commands;
[MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null)
{
[Key("CommandType")]
public CommandType CommandType { get; set; } = commandType;
[Key("Data")]
public byte[]? Data { get; set; } = data;
public static RemoteCommand Create(CommandType commandType) =>
new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data));
}
[MessagePackObject]
public class LoadFileData(string filename, string? folder = null )
{
[Key(nameof(Folder))]
public string? Folder { get; set; } = folder;
[Key(nameof(Filename))]
public string Filename { get; set; } = filename;
}
public enum CommandType
{
None = 0,
Login = 10,
Load = 20,
Inference = 30,
StopInference = 40,
Exit = 100
}
@@ -1,10 +1,16 @@
namespace Azaion.Common.DTO.Config; namespace Azaion.CommonSecurity.DTO;
public class DirectoriesConfig public class DirectoriesConfig
{ {
public string ApiResourcesDirectory { get; set; } = null!;
public string VideosDirectory { get; set; } = null!; public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!; public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!; public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!; public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!; public string ThumbnailsDirectory { get; set; } = null!;
public string GpsSatDirectory { get; set; } = null!;
public string GpsRouteDirectory { get; set; } = null!;
} }
@@ -0,0 +1,19 @@
namespace Azaion.CommonSecurity.DTO;
public abstract class ExternalClientConfig
{
public string ZeroMqHost { get; set; } = "";
public int ZeroMqPort { get; set; }
public double OneTryTimeoutSeconds { get; set; }
public int RetryCount {get;set;}
}
public class InferenceClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; }
}
public class GpsDeniedClientConfig : ExternalClientConfig
{
public int ZeroMqSubscriberPort { get; set; }
}
@@ -6,6 +6,4 @@ public class HardwareInfo
public string GPU { get; set; } = null!; public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!; public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!; public string Memory { get; set; } = null!;
public string Hash { get; set; } = null!;
} }
+9 -2
View File
@@ -1,4 +1,6 @@
namespace Azaion.CommonSecurity.DTO; using Azaion.Common.Extensions;
namespace Azaion.CommonSecurity.DTO;
public enum RoleEnum public enum RoleEnum
{ {
@@ -7,6 +9,11 @@ public enum RoleEnum
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue. Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
CompanionPC = 30, CompanionPC = 30,
Admin = 40, // Admin = 40, //
ResourceUploader = 50, //Uploading dll and ai models
ApiAdmin = 1000 //everything ApiAdmin = 1000 //everything
} }
public static class RoleEnumExtensions
{
public static bool IsValidator(this RoleEnum role) =>
role.In(RoleEnum.Validator, RoleEnum.Admin, RoleEnum.ApiAdmin);
}
+3 -1
View File
@@ -2,5 +2,7 @@
public class SecureAppConfig public class SecureAppConfig
{ {
public ApiConfig ApiConfig { get; set; } = null!; public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
} }
+15 -15
View File
@@ -1,21 +1,21 @@
using System.Security.Claims;
namespace Azaion.CommonSecurity.DTO; namespace Azaion.CommonSecurity.DTO;
public class User public class User
{ {
public Guid Id { get; set; } public string Id { get; set; } = "";
public string Email { get; set; } public string Email { get; set; } = "";
public RoleEnum Role { get; set; } public RoleEnum Role { get; set; }
public UserConfig? UserConfig { get; set; } = null!;
public User(IEnumerable<Claim> claims) }
{
var claimDict = claims.ToDictionary(x => x.Type, x => x.Value); public class UserConfig
{
Id = Guid.Parse(claimDict[SecurityConstants.CLAIM_NAME_ID]); public UserQueueOffsets? QueueOffsets { get; set; } = new();
Email = claimDict[SecurityConstants.CLAIM_EMAIL]; }
if (!Enum.TryParse(claimDict[SecurityConstants.CLAIM_ROLE], out RoleEnum role))
role = RoleEnum.None; public class UserQueueOffsets
Role = role; {
} public ulong AnnotationsOffset { get; set; }
public ulong AnnotationsConfirmOffset { get; set; }
public ulong AnnotationsCommandsOffset { get; set; }
} }
+46 -9
View File
@@ -1,18 +1,55 @@
namespace Azaion.CommonSecurity; using Azaion.CommonSecurity.DTO;
namespace Azaion.CommonSecurity;
public class SecurityConstants public class SecurityConstants
{ {
public const string CONFIG_PATH = "config.json"; public const string CONFIG_PATH = "config.json";
#region ApiConfig public const string DUMMY_DIR = "dummy";
public const string DEFAULT_API_URL = "https://api.azaion.com/"; #region ExternalClientsConfig
public const int DEFAULT_API_RETRY_COUNT = 3;
public const int DEFAULT_API_TIMEOUT_SECONDS = 40;
public const string CLAIM_NAME_ID = "nameid"; public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string CLAIM_EMAIL = "unique_name"; public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public const string CLAIM_ROLE = "role"; public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
#endregion ApiConfig public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227;
public const int DEFAULT_RETRY_COUNT = 25;
public const int DEFAULT_TIMEOUT_SECONDS = 5;
# region Cache keys
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string HARDWARE_INFO_KEY = "HardwareInfo";
# endregion
public static readonly SecureAppConfig DefaultSecureAppConfig = new()
{
InferenceClientConfig = new InferenceClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT
},
GpsDeniedClientConfig = new GpsDeniedClientConfig
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS,
RetryCount = DEFAULT_RETRY_COUNT,
},
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
}
};
#endregion ExternalClientsConfig
} }
@@ -0,0 +1,123 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json;
namespace Azaion.CommonSecurity.Services;
public interface IAzaionApi
{
ApiCredentials Credentials { get; }
User CurrentUser { get; }
void UpdateOffsets(UserQueueOffsets offsets);
//Stream GetResource(string filename, string folder);
}
public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
{
private string _jwtToken = null!;
const string APP_JSON = "application/json";
public ApiCredentials Credentials => credentials;
public User CurrentUser
{
get
{
var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY,
() => Get<User>("currentUser"));
if (user == null)
throw new Exception("Can't get current user");
return user;
}
}
public void UpdateOffsets(UserQueueOffsets offsets)
{
Put($"/users/queue-offsets/set", new
{
Email = CurrentUser.Email,
Offsets = offsets
});
}
private HttpResponseMessage Send(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_jwtToken))
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var response = client.Send(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
response = client.Send(request);
}
if (response.IsSuccessStatusCode)
return response;
var stream = response.Content.ReadAsStream();
var content = new StreamReader(stream).ReadToEnd();
if (response.StatusCode == HttpStatusCode.Conflict)
{
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
}
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
}
private T? Get<T>(string url)
{
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
return JsonConvert.DeserializeObject<T>(json);
}
private void Put<T>(string url, T obj)
{
Send(new HttpRequestMessage(HttpMethod.Put, url)
{
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
});
}
private void Authorize()
{
try
{
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
var payload = new
{
email = credentials.Email,
password = credentials.Password
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
var response = client.Send(message);
if (!response.IsSuccessStatusCode)
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
_jwtToken = result.Token;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
@@ -1,127 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json;
namespace Azaion.CommonSecurity.Services;
public class AzaionApiClient(HttpClient httpClient) : IDisposable
{
const string JSON_MEDIA = "application/json";
private string Email { get; set; } = null!;
private SecureString Password { get; set; } = new();
private string JwtToken { get; set; } = null!;
public User User { get; set; } = null!;
public static AzaionApiClient Create(ApiCredentials credentials)
{
ApiConfig apiConfig;
try
{
if (!File.Exists(SecurityConstants.CONFIG_PATH))
throw new FileNotFoundException(SecurityConstants.CONFIG_PATH);
var configStr = File.ReadAllText(SecurityConstants.CONFIG_PATH);
apiConfig = JsonConvert.DeserializeObject<SecureAppConfig>(configStr)!.ApiConfig;
}
catch (Exception e)
{
Console.WriteLine(e);
apiConfig = new ApiConfig
{
Url = SecurityConstants.DEFAULT_API_URL,
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT ,
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
};
}
var api = new AzaionApiClient(new HttpClient
{
BaseAddress = new Uri(apiConfig.Url),
Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds)
});
api.EnterCredentials(credentials);
return api;
}
public void EnterCredentials(ApiCredentials credentials)
{
if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password))
throw new Exception("Email or password is empty!");
Email = credentials.Email;
Password = credentials.Password.ToSecureString();
}
public async Task<Stream> GetResource(string fileName, string password, HardwareInfo hardware)
{
var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get")
{
Content = new StringContent(JsonConvert.SerializeObject(new { fileName, password, hardware }), Encoding.UTF8, JSON_MEDIA)
});
return await response.Content.ReadAsStreamAsync();
}
private async Task Authorize()
{
if (string.IsNullOrEmpty(Email) || Password.Length == 0)
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
var payload = new
{
email = Email,
password = Password.ToRealString()
};
var response = await httpClient.PostAsync(
"login",
new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, JSON_MEDIA));
if (!response.IsSuccessStatusCode)
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
var responseData = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<LoginResponse>(responseData);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(result.Token);
User = new User(token.Claims);
JwtToken = result.Token;
}
private async Task<HttpResponseMessage> Send(HttpClient client, HttpRequestMessage request)
{
if (string.IsNullOrEmpty(JwtToken))
await Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
var response = await client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
await Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
response = await client.SendAsync(request);
}
if (response.IsSuccessStatusCode)
return response;
var result = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed: {response.StatusCode}! Result: {result}");
}
public void Dispose()
{
httpClient.Dispose();
Password.Dispose();
}
}
+27
View File
@@ -0,0 +1,27 @@
using LazyCache;
namespace Azaion.CommonSecurity.Services;
public interface ICache
{
T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null);
void Invalidate(string key);
}
public class MemoryCache : ICache
{
private readonly IAppCache _cache = new CachingService();
public T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null)
{
expiration ??= TimeSpan.FromHours(4);
return _cache.GetOrAdd(key, entry =>
{
var result = fetchFunc();
entry.AbsoluteExpirationRelativeToNow = expiration;
return result;
});
}
public void Invalidate(string key) => _cache.Remove(key);
}
@@ -8,101 +8,96 @@ namespace Azaion.CommonSecurity.Services;
public interface IHardwareService public interface IHardwareService
{ {
HardwareInfo GetHardware(); //HardwareInfo GetHardware();
} }
public class HardwareService : IHardwareService public class HardwareService : IHardwareService
{ {
private const string WIN32_GET_HARDWARE_COMMAND = // private const string WIN32_GET_HARDWARE_COMMAND =
"wmic OS get TotalVisibleMemorySize /Value && " + // "powershell -Command \"" +
"wmic CPU get Name /Value && " + // "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " +
"wmic path Win32_VideoController get Name /Value"; // "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " +
// "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" +
// "\"";
//
// private const string UNIX_GET_HARDWARE_COMMAND =
// "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
// "lscpu | grep 'Model name:' | cut -d':' -f2 && " +
// "lspci | grep VGA | cut -d':' -f3\"";
private const string UNIX_GET_HARDWARE_COMMAND = // public HardwareInfo GetHardware()
"/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " + // {
"lscpu | grep 'Model name:' | cut -d':' -f2 && " + // try
"lspci | grep VGA | cut -d':' -f3\""; // {
// var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
// ? WIN32_GET_HARDWARE_COMMAND
// : UNIX_GET_HARDWARE_COMMAND);
//
// var lines = output
// .Replace("TotalVisibleMemorySize=", "")
// .Replace("Name=", "")
// .Replace(" ", " ")
// .Trim()
// .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries)
// .Select(x => x.Trim())
// .ToArray();
//
// if (lines.Length < 3)
// throw new Exception("Can't get hardware info");
//
// var hardwareInfo = new HardwareInfo
// {
// CPU = lines[0],
// GPU = lines[1],
// Memory = lines[2],
// MacAddress = GetMacAddress()
// };
// return hardwareInfo;
// }
// catch (Exception ex)
// {
// Console.WriteLine(ex.Message);
// throw;
// }
// }
public HardwareInfo GetHardware() // private string GetMacAddress()
{ // {
try // var macAddress = NetworkInterface
{ // .GetAllNetworkInterfaces()
var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT // .Where(nic => nic.OperationalStatus == OperationalStatus.Up)
? WIN32_GET_HARDWARE_COMMAND // .Select(nic => nic.GetPhysicalAddress().ToString())
: UNIX_GET_HARDWARE_COMMAND); // .FirstOrDefault();
//
// return macAddress ?? string.Empty;
// }
//
// private string RunCommand(string command)
// {
// try
// {
// using var process = new Process();
// process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
// process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
// ? $"-c \"{command}\""
// : $"/c {command}";
// process.StartInfo.RedirectStandardOutput = true;
// process.StartInfo.UseShellExecute = false;
// process.StartInfo.CreateNoWindow = true;
//
// process.Start();
// var result = process.StandardOutput.ReadToEnd();
// process.WaitForExit();
//
// return result.Trim();
// }
// catch
// {
// return string.Empty;
// }
// }
var lines = output // private static string ToHash(string str) =>
.Replace("TotalVisibleMemorySize=", "") // Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
.Replace("Name=", "")
.Replace(" ", " ")
.Trim()
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
var memoryStr = "Unknown RAM";
if (lines.Length > 0)
{
memoryStr = lines[0];
if (int.TryParse(memoryStr, out var memKb))
memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb";
}
var hardwareInfo = new HardwareInfo
{
Memory = memoryStr,
CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1])
? "Unknown RAM"
: lines[1],
GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2])
? "Unknown GPU"
: lines[2]
};
hardwareInfo.Hash = ToHash($"Azaion_{MacAddress()}_{hardwareInfo.CPU}_{hardwareInfo.GPU}");
return hardwareInfo;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
}
private string MacAddress()
{
var macAddress = NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
.Select(nic => nic.GetPhysicalAddress().ToString())
.FirstOrDefault();
return macAddress ?? string.Empty;
}
private string RunCommand(string command)
{
try
{
using var process = new Process();
process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
? $"-c \"{command}\""
: $"/c {command}";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
var result = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return result.Trim();
}
catch
{
return string.Empty;
}
}
private static string ToHash(string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
} }
@@ -0,0 +1,104 @@
using System.Diagnostics;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.CommonSecurity.Services;
public interface IInferenceClient
{
void Send(RemoteCommand create);
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> config)
{
_inferenceClientConfig = config.Value;
Start();
_ = Task.Run(ProcessClientCommands);
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
}
private async Task ProcessClientCommands()
{
//TODO: implement always on ready to client's requests. Utilize RemoteCommand
await Task.CompletedTask;
}
public void Stop()
{
if (!_dealer.IsDisposed)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Close();
}
}
public void Send(RemoteCommand command)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
}
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
{
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
}
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum < retries)
{
tryNum++;
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
continue;
return bytes;
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each");
return null;
}
}
@@ -1,58 +1,22 @@
using System.Reflection; using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.DependencyInjection;
namespace Azaion.CommonSecurity.Services; namespace Azaion.CommonSecurity.Services;
public interface IResourceLoader public interface IResourceLoader
{ {
Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default); MemoryStream LoadFile(string fileName, string? folder = null);
Assembly? LoadAssembly(string asmName);
} }
public class ResourceLoader(AzaionApiClient api, ApiCredentials credentials) : IResourceLoader public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader
{ {
private static readonly List<string> EncryptedResources = public MemoryStream LoadFile(string fileName, string? folder = null)
[
"Azaion.Annotator",
"Azaion.Dataset"
];
public Assembly? LoadAssembly(string resourceName)
{ {
var assemblyName = resourceName.Split(',').First(); inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder)));
if (EncryptedResources.Contains(assemblyName)) var bytes = inferenceClient.GetBytes(2, 3);
{ if (bytes == null)
try throw new Exception($"Unable to receive {fileName}");
{
var stream = Load($"{assemblyName}.dll").GetAwaiter().GetResult();
return Assembly.Load(stream.ToArray());
}
catch (Exception e)
{
Console.WriteLine(e);
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dllPath = Path.Combine(currentLocation, "dummy", $"{assemblyName}.dll");
return Assembly.LoadFile(dllPath);
}
}
var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies() return new MemoryStream(bytes);
.FirstOrDefault(a => a.GetName().Name == assemblyName);
return loadedAssembly;
}
public async Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default)
{
var hardwareService = new HardwareService();
var hardwareInfo = hardwareService.GetHardware();
var encryptedStream = Task.Run(() => api.GetResource(fileName, credentials.Password, hardwareInfo), cancellationToken).Result;
var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash);
var stream = new MemoryStream();
await encryptedStream.DecryptTo(stream, key, cancellationToken);
stream.Seek(0, SeekOrigin.Begin);
return stream;
} }
} }
@@ -1,82 +0,0 @@
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Text;
namespace Azaion.CommonSecurity.Services;
public static class Security
{
private const int BUFFER_SIZE = 524288; // 512 KB buffer size
public static string ToHash(this string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
public static string MakeEncryptionKey(string email, string password, string? hardwareHash) =>
$"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash();
public static SecureString ToSecureString(this string str)
{
var secureString = new SecureString();
foreach (var c in str.ToCharArray())
secureString.AppendChar(c);
return secureString;
}
public static string? ToRealString(this SecureString value)
{
var valuePtr = IntPtr.Zero;
try
{
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
return Marshal.PtrToStringUni(valuePtr);
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
}
}
public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default)
{
if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream));
if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key));
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true);
// Prepend IV to the encrypted data
await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken);
var buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0)
await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default)
{
using var aes = Aes.Create();
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
// Read the IV from the start of the input stream
var iv = new byte[aes.BlockSize / 8];
_ = await encryptedStream.ReadAsync(iv, cancellationToken);
aes.IV = iv;
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true);
// Read and write in chunks
var buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0)
await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
}
}
+80 -12
View File
@@ -12,7 +12,21 @@
WindowState="Maximized"> WindowState="Maximized">
<Window.Resources> <Window.Resources>
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationImageView}"> <DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:AnnotationThumbnail}">
<Border BorderBrush="IndianRed" Padding="5">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderThickness" Value="0"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSeed}" Value="True">
<Setter Property="BorderThickness" Value="8"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsSeed}" Value="False">
<Setter Property="BorderThickness" Value="0"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
@@ -29,6 +43,7 @@
Foreground="LightGray" Foreground="LightGray"
Text="{Binding ImageName}" /> Text="{Binding ImageName}" />
</Grid> </Grid>
</Border>
</DataTemplate> </DataTemplate>
</Window.Resources> </Window.Resources>
@@ -42,16 +57,13 @@
<RowDefinition Height="32"></RowDefinition> <RowDefinition Height="32"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="150" /> <ColumnDefinition Width="250" />
<ColumnDefinition Width="4"/> <ColumnDefinition Width="4"/>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<controls:DetectionClasses <controls:DetectionClasses x:Name="LvClasses"
x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="0"> Grid.Row="0" />
</controls:DetectionClasses>
<TabControl <TabControl
Name="Switcher" Name="Switcher"
Grid.Column="2" Grid.Column="2"
@@ -108,10 +120,65 @@
</Grid> </Grid>
</ItemsPanelTemplate> </ItemsPanelTemplate>
</StatusBar.ItemsPanel> </StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="2" Background="Black"> <StatusBarItem Grid.Column="0" Background="Black">
<TextBlock Name="RefreshThumbCaption">База іконок:</TextBlock> <Button Name="ValidateBtn"
Padding="2"
ToolTip="Підтвердити валідність. Клавіша: [V]"
Background="Black" BorderBrush="Black"
Cursor="Hand"
Click="ValidateAnnotationsClick">
<StackPanel Orientation="Horizontal">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m30.71 7.29-6-6a1 1 0 0 0 -.71-.29h-2v8a2 2 0 0 1 -2 2h-8a2 2 0 0
1 -2-2v-8h-6a3 3 0 0 0 -3 3v24a3 3 0 0 0 3 3h2v-9a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v9h2a3 3 0 0 0 3-3v-20a1 1 0 0 0 -.29-.71z" />
<GeometryDrawing Brush="LightGray" Geometry="m12 1h8v8h-8z" />
<GeometryDrawing Brush="LightGray" Geometry="m23 21h-14a1 1 0 0 0 -1 1v9h16v-9a1 1 0 0 0 -1-1z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Text="Підтвердити валідність" Foreground="White" Padding="8 0 0 0"></TextBlock>
</StackPanel>
</Button>
</StatusBarItem> </StatusBarItem>
<StatusBarItem Grid.Column="3" Background="Black">
<Separator Grid.Column="1" />
<StatusBarItem x:Name="RefreshThumbnailsButtonItem" Grid.Column="2" Background="Black">
<Button
Padding="2"
Height="25"
ToolTip="Оновити базу іконок" Background="Black"
BorderBrush="Black"
Cursor="Hand"
Click="RefreshThumbnailsBtnClick">
<StackPanel Orientation="Horizontal">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V1200 H1200 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="M889.68 166.32c-93.608-102.216-228.154-166.32-377.68-166.32-282.77 0-512
229.23-512 512h96c0-229.75 186.25-416 416-416 123.020 0 233.542 53.418 309.696 138.306l-149.696 149.694h352v-352l-134.32 134.32z" />
<GeometryDrawing Brush="LightGray" Geometry="M928 512c0 229.75-186.25 416-416 416-123.020
0-233.542-53.418-309.694-138.306l149.694-149.694h-352v352l134.32-134.32c93.608 102.216 228.154 166.32 377.68 166.32 282.77 0 512-229.23 512-512h-96z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Foreground="White" Padding="8 0 0 0">Оновити базу іконок</TextBlock>
</StackPanel>
</Button>
</StatusBarItem>
<StatusBarItem Grid.Column="2" x:Name="RefreshProgressBarItem" Visibility="Hidden">
<StackPanel>
<TextBlock Name="RefreshThumbCaption" Padding="0 0 5 0">База іконок:</TextBlock>
<ProgressBar x:Name="RefreshThumbBar" <ProgressBar x:Name="RefreshThumbBar"
Width="150" Width="150"
Height="15" Height="15"
@@ -123,9 +190,10 @@
Minimum="0" Minimum="0"
Value="0"> Value="0">
</ProgressBar> </ProgressBar>
</StackPanel>
</StatusBarItem> </StatusBarItem>
<Separator Grid.Column="4"/>
<StatusBarItem Grid.Column="5" Background="Black"> <StatusBarItem Grid.Column="3" Background="Black">
<TextBlock Name="StatusText" Text=""/> <TextBlock Name="StatusText" Text=""/>
</StatusBarItem> </StatusBarItem>
</StatusBar> </StatusBar>
+119 -110
View File
@@ -1,13 +1,13 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO;
using LinqToDB; using LinqToDB;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,26 +17,30 @@ using Color = ScottPlot.Color;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEvent> public partial class DatasetExplorer
{ {
private readonly ILogger<DatasetExplorer> _logger; private readonly ILogger<DatasetExplorer> _logger;
private readonly AnnotationConfig _annotationConfig; private readonly AnnotationConfig _annotationConfig;
private readonly DirectoriesConfig _directoriesConfig; private readonly DirectoriesConfig _directoriesConfig;
private Dictionary<int, List<Annotation>> _annotationsDict; private readonly Dictionary<int, Dictionary<string, Annotation>> _annotationsDict;
private readonly CancellationTokenSource _cts = new();
public ObservableCollection<AnnotationImageView> SelectedAnnotations { get; set; } = new(); public List<DetectionClass> AllDetectionClasses { get; set; }
private ObservableCollection<DetectionClass> AllAnnotationClasses { get; set; } = new(); public ObservableCollection<AnnotationThumbnail> SelectedAnnotations { get; set; } = new();
public readonly Dictionary<string, AnnotationThumbnail> SelectedAnnotationDict = new();
public Dictionary<string, LabelInfo> LabelsCache { get; set; } = new();
private int _tempSelectedClassIdx = 0; private int _tempSelectedClassIdx = 0;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IMediator _mediator;
public readonly List<DetectionClass> AnnotationsClasses;
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
public AnnotationImageView? CurrentAnnotation { get; set; } public AnnotationThumbnail? CurrentAnnotation { get; set; }
public DatasetExplorer( public DatasetExplorer(
IOptions<DirectoriesConfig> directoriesConfig, IOptions<DirectoriesConfig> directoriesConfig,
@@ -44,15 +48,25 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
ILogger<DatasetExplorer> logger, ILogger<DatasetExplorer> logger,
IGalleryService galleryService, IGalleryService galleryService,
FormState formState, FormState formState,
IDbFactory dbFactory) IDbFactory dbFactory,
IMediator mediator)
{ {
InitializeComponent();
_directoriesConfig = directoriesConfig.Value; _directoriesConfig = directoriesConfig.Value;
_annotationConfig = annotationConfig.Value; _annotationConfig = annotationConfig.Value;
_logger = logger; _logger = logger;
_galleryService = galleryService; _galleryService = galleryService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_mediator = mediator;
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id))
.ToDictionary(x => x, _ => new Dictionary<string, Annotation>());
_annotationsDict.Add(-1, []);
AnnotationsClasses = annotationConfig.Value.DetectionClasses;
InitializeComponent();
Loaded += OnLoaded; Loaded += OnLoaded;
Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer; Activated += (_, _) => formState.ActiveWindow = WindowEnum.DatasetExplorer;
@@ -61,102 +75,87 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
switch (args.Key) switch (args.Key)
{ {
case Key.Delete: case Key.Delete:
DeleteAnnotations(); await DeleteAnnotations();
break; break;
case Key.Enter: case Key.Enter:
await EditAnnotation(); await EditAnnotation(ThumbnailsView.SelectedIndex);
break; break;
} }
}; };
ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(); ThumbnailsView.MouseDoubleClick += async (_, _) => await EditAnnotation(ThumbnailsView.SelectedIndex);
ThumbnailsView.SelectionChanged += (_, _) => ThumbnailsView.SelectionChanged += (_, _) =>
{ {
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed)
? Visibility.Visible
: Visibility.Hidden;
}; };
ExplorerEditor.GetTimeFunc = () => CurrentAnnotation!.Annotation.Time;
ExplorerEditor.GetTimeFunc = () => Constants.GetTime(CurrentAnnotation!.Annotation.ImagePath); _galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
galleryService.ThumbnailsUpdate += thumbnailsPercentage =>
{ {
Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage); Dispatcher.Invoke(() => RefreshThumbBar.Value = thumbnailsPercentage);
}; };
Closing += (_, _) => _cts.Cancel();
AllDetectionClasses = new List<DetectionClass>(
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.DetectionClasses));
LvClasses.Init(AllDetectionClasses);
_dbFactory.Run(async db =>
{
var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections)
.OrderBy(x => x.AnnotationStatus)
.ThenByDescending(x => x.CreatedDate)
.ToListAsync();
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
}).GetAwaiter().GetResult();
DataContext = this;
} }
private async void OnLoaded(object sender, RoutedEventArgs e) private async void OnLoaded(object sender, RoutedEventArgs e)
{ {
AllAnnotationClasses = new ObservableCollection<DetectionClass>( LvClasses.DetectionClassChanged += async (_, args) =>
new List<DetectionClass> { new() {Id = -1, Name = "All", ShortName = "All"}}
.Concat(_annotationConfig.AnnotationClasses));
LvClasses.ItemsSource = AllAnnotationClasses;
LvClasses.MouseUp += async (_, _) =>
{ {
var selectedClass = (DetectionClass)LvClasses.SelectedItem; ExplorerEditor.CurrentAnnClass = args.DetectionClass;
ExplorerEditor.CurrentAnnClass = selectedClass;
_annotationConfig.LastSelectedExplorerClass = selectedClass.Id;
if (Switcher.SelectedIndex == 0) if (Switcher.SelectedIndex == 0)
await ReloadThumbnails(); await ReloadThumbnails();
else else
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected)) foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
ann.DetectionClass = selectedClass; ann.DetectionClass = args.DetectionClass;
}; };
LvClasses.SelectionChanged += (_, _) => ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First();
{
if (Switcher.SelectedIndex != 1)
return;
var selectedClass = (DetectionClass)LvClasses.SelectedItem;
if (selectedClass == null)
return;
ExplorerEditor.CurrentAnnClass = selectedClass;
foreach (var ann in ExplorerEditor.CurrentDetections.Where(x => x.IsSelected))
ann.DetectionClass = selectedClass;
};
LvClasses.SelectedIndex = _annotationConfig.LastSelectedExplorerClass ?? 0;
ExplorerEditor.CurrentAnnClass = (DetectionClass)LvClasses.SelectedItem;
await _dbFactory.Run(async db =>
{
var allAnnotations = await db.Annotations
.LoadWith(x => x.Detections)
.OrderByDescending(x => x.CreatedDate)
.ToListAsync();
_annotationsDict = AllAnnotationClasses.ToDictionary(x => x.Id, _ => new List<Annotation>());
foreach (var annotation in allAnnotations)
AddAnnotationToDict(annotation);
});
await ReloadThumbnails(); await ReloadThumbnails();
await LoadClassDistribution(); await LoadClassDistribution();
RefreshThumbBar.Value = _galleryService.ProcessedThumbnailsPercentage;
DataContext = this; DataContext = this;
} }
private void AddAnnotationToDict(Annotation annotation) public void AddAnnotationToDict(Annotation annotation)
{ {
foreach (var c in annotation.Classes) foreach (var c in annotation.Classes)
_annotationsDict[c].Add(annotation); _annotationsDict[c][annotation.Name] = annotation;
_annotationsDict[-1].Add(annotation); _annotationsDict[-1][annotation.Name] = annotation;
} }
private async Task LoadClassDistribution() private async Task LoadClassDistribution()
{ {
var data = LabelsCache var data = _annotationsDict
.SelectMany(x => x.Value.Classes) .Where(x => x.Key != -1)
.GroupBy(x => x) .Select(gr => new
.Select(x => new
{ {
x.Key, gr.Key,
_annotationConfig.DetectionClassesDict[x.Key].Name, _annotationConfig.DetectionClassesDict[gr.Key].ShortName,
_annotationConfig.DetectionClassesDict[x.Key].Color, _annotationConfig.DetectionClassesDict[gr.Key].Color,
ClassCount = x.Count() ClassCount = gr.Value.Count
}) })
.ToList(); .ToList();
@@ -177,7 +176,7 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
foreach (var x in data) foreach (var x in data)
{ {
var label = ClassDistribution.Plot.Add.Text(x.Name, 50, -1.5 * x.Key + 1.1); var label = ClassDistribution.Plot.Add.Text(x.ShortName, 50, -1.5 * x.Key + 1.1);
label.LabelFontColor = foregroundColor; label.LabelFontColor = foregroundColor;
label.LabelFontSize = 18; label.LabelFontSize = 18;
} }
@@ -187,28 +186,36 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
ClassDistribution.Plot.FigureBackground.Color = new("#888888"); ClassDistribution.Plot.FigureBackground.Color = new("#888888");
ClassDistribution.Refresh(); ClassDistribution.Refresh();
await Task.CompletedTask;
} }
private void ReloadThumbnailsItemClick(object sender, RoutedEventArgs e) private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e)
{ {
var result = MessageBox.Show($"Видалити всі іконки і згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?", RefreshThumbnailsButtonItem.Visibility = Visibility.Hidden;
RefreshProgressBarItem.Visibility = Visibility.Visible;
var result = MessageBox.Show($"Видалити всі іконки та згенерувати нову базу іконок в {_directoriesConfig.ThumbnailsDirectory}?",
"Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question); "Підтвердження оновлення іконок", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
_galleryService.ClearThumbnails(); await _galleryService.ClearThumbnails();
_galleryService.RefreshThumbnails(); await _galleryService.RefreshThumbnails();
RefreshProgressBarItem.Visibility = Visibility.Hidden;
RefreshThumbnailsButtonItem.Visibility = Visibility.Visible;
} }
private async Task EditAnnotation() public async Task EditAnnotation(int index)
{ {
try try
{ {
ThumbnailLoading = true; ThumbnailLoading = true;
if (index == -1)
if (ThumbnailsView.SelectedItem == null)
return; return;
CurrentAnnotation = (ThumbnailsView.SelectedItem as AnnotationImageView)!; CurrentAnnotation = (ThumbnailsView.Items[index] as AnnotationThumbnail)!;
ThumbnailsView.SelectedIndex = index;
var ann = CurrentAnnotation.Annotation; var ann = CurrentAnnotation.Annotation;
ExplorerEditor.Background = new ImageBrush ExplorerEditor.Background = new ImageBrush
{ {
@@ -216,16 +223,9 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
}; };
SwitchTab(toEditor: true); SwitchTab(toEditor: true);
var time = Constants.GetTime(ann.ImagePath); var time = ann.Time;
ExplorerEditor.RemoveAllAnns(); ExplorerEditor.RemoveAllAnns();
foreach (var deetection in ann.Detections) ExplorerEditor.CreateDetections(time, ann.Detections, _annotationConfig.DetectionClasses, ExplorerEditor.RenderSize);
{
var annClass = _annotationConfig.DetectionClassesDict[deetection.ClassNumber];
var canvasLabel = new CanvasLabel(deetection, ExplorerEditor.RenderSize, ExplorerEditor.RenderSize);
ExplorerEditor.CreateAnnotation(annClass, time, canvasLabel);
}
ThumbnailLoading = false;
} }
catch (Exception e) catch (Exception e)
{ {
@@ -234,7 +234,11 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
} }
finally finally
{ {
_ = Task.Run(async () =>
{
await Task.Delay(100);
ThumbnailLoading = false; ThumbnailLoading = false;
});
} }
} }
@@ -245,58 +249,63 @@ public partial class DatasetExplorer : INotificationHandler<AnnotationCreatedEve
{ {
AnnotationsTab.Visibility = Visibility.Collapsed; AnnotationsTab.Visibility = Visibility.Collapsed;
EditorTab.Visibility = Visibility.Visible; EditorTab.Visibility = Visibility.Visible;
_tempSelectedClassIdx = LvClasses.SelectedIndex; _tempSelectedClassIdx = LvClasses.CurrentClassNumber;
LvClasses.ItemsSource = _annotationConfig.AnnotationClasses; LvClasses.DetectionDataGrid.ItemsSource = _annotationConfig.DetectionClasses;
Switcher.SelectedIndex = 1; Switcher.SelectedIndex = 1;
LvClasses.SelectedIndex = Math.Max(0, _tempSelectedClassIdx - 1); LvClasses.SelectNum(Math.Max(0, _tempSelectedClassIdx - 1));
} }
else else
{ {
AnnotationsTab.Visibility = Visibility.Visible; AnnotationsTab.Visibility = Visibility.Visible;
EditorTab.Visibility = Visibility.Collapsed; EditorTab.Visibility = Visibility.Collapsed;
LvClasses.ItemsSource = AllAnnotationClasses; LvClasses.DetectionDataGrid.ItemsSource = AllDetectionClasses;
LvClasses.SelectedIndex = _tempSelectedClassIdx; LvClasses.SelectNum(_tempSelectedClassIdx);
Switcher.SelectedIndex = 0; Switcher.SelectedIndex = 0;
} }
} }
private void DeleteAnnotations() public async Task DeleteAnnotations()
{ {
var tempSelected = ThumbnailsView.SelectedIndex; var tempSelected = ThumbnailsView.SelectedIndex;
var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question); var result = MessageBox.Show("Чи дійсно видалити аннотації?","Підтвердження видалення", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
var selected = ThumbnailsView.SelectedItems.Count; var annotations = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation)
for (var i = 0; i < selected; i++) .ToList();
{
var dto = (ThumbnailsView.SelectedItems[0] as AnnotationImageView)!; await _mediator.Publish(new AnnotationsDeletedEvent(annotations));
dto.Delete();
SelectedAnnotations.Remove(dto);
}
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
} }
private async Task ReloadThumbnails() private async Task ReloadThumbnails()
{ {
SelectedAnnotations.Clear(); SelectedAnnotations.Clear();
foreach (var ann in _annotationsDict[ExplorerEditor.CurrentAnnClass.Id]) SelectedAnnotationDict.Clear();
SelectedAnnotations.Add(new AnnotationImageView(ann)); var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId];
foreach (var ann in annotations.OrderByDescending(x => x.Value.CreatedDate))
{
var annThumb = new AnnotationThumbnail(ann.Value);
SelectedAnnotations.Add(annThumb);
SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
}
await Task.CompletedTask;
} }
private async void ValidateAnnotationsClick(object sender, RoutedEventArgs e)
private void AddThumbnail(Annotation annotation)
{ {
var selectedClass = ((DetectionClass?)LvClasses.SelectedItem)?.Id; var result = MessageBox.Show("Підтверджуєте валідність обраних аннотацій?","Підтвердження валідності", MessageBoxButton.OKCancel, MessageBoxImage.Question);
if (selectedClass == null) if (result != MessageBoxResult.OK)
return; return;
AddAnnotationToDict(annotation); try
if (annotation.Classes.Contains(selectedClass.Value)) {
SelectedAnnotations.Add(new AnnotationImageView(annotation)); await _mediator.Publish(new DatasetExplorerControlEvent(PlaybackControlEnum.ValidateAnnotations), _cts.Token);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
} }
public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) =>
AddThumbnail(notification.Annotation);
} }
+93 -12
View File
@@ -1,26 +1,40 @@
using System.IO; using System.IO;
using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using MediatR; using MediatR;
using Microsoft.Extensions.Options;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public class DatasetExplorerEventHandler( public class DatasetExplorerEventHandler(
DatasetExplorer datasetExplorer, DatasetExplorer datasetExplorer,
AnnotationService annotationService) : INotificationHandler<KeyEvent> AnnotationService annotationService) :
INotificationHandler<KeyEvent>,
INotificationHandler<DatasetExplorerControlEvent>,
INotificationHandler<AnnotationCreatedEvent>,
INotificationHandler<AnnotationsDeletedEvent>
{ {
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new() private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{ {
{ Key.Enter, PlaybackControlEnum.SaveAnnotations }, { Key.Enter, PlaybackControlEnum.SaveAnnotations },
{ Key.Delete, PlaybackControlEnum.RemoveSelectedAnns }, { Key.Delete, PlaybackControlEnum.RemoveSelectedAnns },
{ Key.X, PlaybackControlEnum.RemoveAllAnns }, { Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close } { Key.Escape, PlaybackControlEnum.Close },
{ Key.Down, PlaybackControlEnum.Next },
{ Key.Up, PlaybackControlEnum.Previous },
{ Key.V, PlaybackControlEnum.ValidateAnnotations},
}; };
public async Task Handle(DatasetExplorerControlEvent notification, CancellationToken cancellationToken)
{
await HandleControl(notification.PlaybackControl, cancellationToken);
}
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken) public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken)
{ {
if (keyEvent.WindowEnum != WindowEnum.DatasetExplorer) if (keyEvent.WindowEnum != WindowEnum.DatasetExplorer)
@@ -33,15 +47,15 @@ public class DatasetExplorerEventHandler(
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1; if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue) if (keyNumber.HasValue)
datasetExplorer.LvClasses.SelectedIndex = keyNumber.Value; datasetExplorer.LvClasses.SelectNum(keyNumber.Value);
else else
{ {
if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value)) if (datasetExplorer.Switcher.SelectedIndex == 1 && _keysControlEnumDict.TryGetValue(key, out var value))
await HandleControl(value); await HandleControl(value, cancellationToken);
} }
} }
private async Task HandleControl(PlaybackControlEnum controlEnum) private async Task HandleControl(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default)
{ {
switch (controlEnum) switch (controlEnum)
{ {
@@ -49,24 +63,91 @@ public class DatasetExplorerEventHandler(
if (datasetExplorer.ThumbnailLoading) if (datasetExplorer.ThumbnailLoading)
return; return;
var fName = Path.GetFileNameWithoutExtension(datasetExplorer.CurrentAnnotation!.Annotation.ImagePath); var a = datasetExplorer.CurrentAnnotation!.Annotation;
var extension = Path.GetExtension(fName);
var detections = datasetExplorer.ExplorerEditor.CurrentDetections var detections = datasetExplorer.ExplorerEditor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize))) .Select(x => new Detection(a.Name, x.GetLabel(datasetExplorer.ExplorerEditor.RenderSize)))
.ToList(); .ToList();
await annotationService.SaveAnnotation(fName, extension, detections, SourceEnum.Manual); var index = datasetExplorer.ThumbnailsView.SelectedIndex;
datasetExplorer.SwitchTab(toEditor: false); await annotationService.SaveAnnotation(a.OriginalMediaName, a.Time, detections, token: cancellationToken);
await datasetExplorer.EditAnnotation(index + 1);
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
if (datasetExplorer.ExplorerEditor.CurrentDetections.Any(x => x.IsSelected))
datasetExplorer.ExplorerEditor.RemoveSelectedAnns(); datasetExplorer.ExplorerEditor.RemoveSelectedAnns();
else
{
await datasetExplorer.DeleteAnnotations();
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex);
}
break; break;
case PlaybackControlEnum.RemoveAllAnns: case PlaybackControlEnum.RemoveAllAnns:
datasetExplorer.ExplorerEditor.RemoveAllAnns(); datasetExplorer.ExplorerEditor.RemoveAllAnns();
break; break;
case PlaybackControlEnum.Next:
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex + 1);
break;
case PlaybackControlEnum.Previous:
await datasetExplorer.EditAnnotation(datasetExplorer.ThumbnailsView.SelectedIndex - 1);
break;
case PlaybackControlEnum.Close: case PlaybackControlEnum.Close:
datasetExplorer.SwitchTab(toEditor: false); datasetExplorer.SwitchTab(toEditor: false);
break; break;
case PlaybackControlEnum.ValidateAnnotations:
var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>()
.Select(x => x.Annotation)
.ToList();
await annotationService.ValidateAnnotations(annotations, cancellationToken);
foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation)))
{
ann.Annotation.AnnotationStatus = AnnotationStatus.Validated;
if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value))
value.Annotation.AnnotationStatus = AnnotationStatus.Validated;
ann.UpdateUI();
} }
break;
}
}
public Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken)
{
datasetExplorer.Dispatcher.Invoke(() =>
{
var annotation = notification.Annotation;
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
//TODO: For editing existing need to handle updates
datasetExplorer.AddAnnotationToDict(annotation);
if (annotation.Classes.Contains(selectedClass) || selectedClass == -1)
{
var annThumb = new AnnotationThumbnail(annotation);
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
{
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name);
if (ann != null)
datasetExplorer.SelectedAnnotations.Remove(ann);
}
datasetExplorer.SelectedAnnotations.Insert(0, annThumb);
datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
}
});
return Task.CompletedTask;
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{
var names = notification.Annotations.Select(x => x.Name).ToList();
var annThumbs = datasetExplorer.SelectedAnnotationDict
.Where(x => names.Contains(x.Key))
.Select(x => x.Value)
.ToList();
foreach (var annThumb in annThumbs)
{
datasetExplorer.SelectedAnnotations.Remove(annThumb);
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
}
await Task.CompletedTask;
} }
} }
+76
View File
@@ -0,0 +1,76 @@
<h2>Azaion AI</h2>
<p>
Azaion AI is a worker written on cython (c-compilable python) which listens to socket and rabbit queue.
It accepts commands om a format:
- CommandType: Inference / Load
- Filename
And correspondingly do inference or just load encrypted file from the API.
Results (file or annotations) is putted to the other queue, or the same socket, depending on the command source.
</p>
<h2>Installation</h2>
Prepare correct onnx model from YOLO:
```python
from ultralytics import YOLO
import netron
model = YOLO("azaion.pt")
model.export(format="onnx", imgsz=1280, nms=True, batch=4)
netron.start('azaion.onnx')
```
Read carefully about [export arguments](https://docs.ultralytics.com/modes/export/), you have to use nms=True, and batching with a proper batch size
<h3>Install libs</h3>
https://www.python.org/downloads/
Windows
- [Install CUDA](https://developer.nvidia.com/cuda-12-1-0-download-archive)
Linux
```
sudo apt install nvidia-driver-535
wget https://developer.download.nvidia.com/compute/cudnn/9.2.0/local_installers/cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb
sudo dpkg -i cudnn-local-repo-ubuntu2204-9.2.0_1.0-1_amd64.deb
sudo cp /var/cudnn-local-repo-ubuntu2204-9.2.0/cudnn-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cudnn nvidia-cuda-toolkit -y
nvcc --version
```
<h3>Install dependencies</h3>
1. Install python with max version 3.11. Pytorch for now supports 3.11 max
Make sure that your virtual env is installed with links to the global python packages and headers, like this:
```
python -m venv --system-site-packages venv
```
This is crucial for the build because build needs Python.h header and other files.
```
python -m pip install --upgrade pip
pip install requirements.txt
pip install pyinstaller
```
In case of fbgemm.dll error (Windows specific):
- copypaste libomp140.x86_64.dll to C:\Windows\System32
<h3>Build</h3>
```
python setup.py build_ext --inplace
```
<h3>Build exe</h3>
```
.\build.cmd
```
+15
View File
@@ -0,0 +1,15 @@
cdef class AIRecognitionConfig:
cdef public double frame_recognition_seconds
cdef public int frame_period_recognition
cdef public double probability_threshold
cdef public double tracking_distance_confidence
cdef public double tracking_probability_increase
cdef public double tracking_intersection_threshold
cdef public bytes file_data
cdef public list[str] paths
cdef public int model_batch_size
@staticmethod
cdef from_msgpack(bytes data)
+52
View File
@@ -0,0 +1,52 @@
from msgpack import unpackb
cdef class AIRecognitionConfig:
def __init__(self,
frame_period_recognition,
frame_recognition_seconds,
probability_threshold,
tracking_distance_confidence,
tracking_probability_increase,
tracking_intersection_threshold,
file_data,
paths,
model_batch_size
):
self.frame_period_recognition = frame_period_recognition
self.frame_recognition_seconds = frame_recognition_seconds
self.probability_threshold = probability_threshold
self.tracking_distance_confidence = tracking_distance_confidence
self.tracking_probability_increase = tracking_probability_increase
self.tracking_intersection_threshold = tracking_intersection_threshold
self.file_data = file_data
self.paths = paths
self.model_batch_size = model_batch_size
def __str__(self):
return (f'frame_seconds : {self.frame_recognition_seconds}, distance_confidence : {self.tracking_distance_confidence}, '
f'probability_increase : {self.tracking_probability_increase}, '
f'intersection_threshold : {self.tracking_intersection_threshold}, '
f'frame_period_recognition : {self.frame_period_recognition}, '
f'paths: {self.paths}, '
f'model_batch_size: {self.model_batch_size}')
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return AIRecognitionConfig(
unpacked.get("f_pr", 0),
unpacked.get("f_rs", 0.0),
unpacked.get("pt", 0.0),
unpacked.get("t_dc", 0.0),
unpacked.get("t_pi", 0.0),
unpacked.get("t_it", 0.0),
unpacked.get("d", b''),
unpacked.get("p", []),
unpacked.get("m_bs")
)
+16
View File
@@ -0,0 +1,16 @@
cdef class Detection:
cdef public double x, y, w, h, confidence
cdef public str annotation_name
cdef public int cls
cdef public overlaps(self, Detection det2)
cdef class Annotation:
cdef public str name
cdef public str original_media_name
cdef long time
cdef public list[Detection] detections
cdef public bytes image
cdef format_time(self, ms)
cdef bytes serialize(self)
+73
View File
@@ -0,0 +1,73 @@
import msgpack
from pathlib import Path
cdef class Detection:
def __init__(self, double x, double y, double w, double h, int cls, double confidence):
self.annotation_name = None
self.x = x
self.y = y
self.w = w
self.h = h
self.cls = cls
self.confidence = confidence
def __str__(self):
return f'{self.cls}: {self.x:.2f} {self.y:.2f} {self.w:.2f} {self.h:.2f}, prob: {(self.confidence*100):.1f}%'
cdef overlaps(self, Detection det2):
cdef double overlap_x = 0.5 * (self.w + det2.w) - abs(self.x - det2.x)
cdef double overlap_y = 0.5 * (self.h + det2.h) - abs(self.y - det2.y)
cdef double overlap_area = max(0.0, overlap_x) * max(0.0, overlap_y)
cdef double min_area = min(self.w * self.h, det2.w * det2.h)
return overlap_area / min_area > 0.6
cdef class Annotation:
def __init__(self, str name, long ms, list[Detection] detections):
self.original_media_name = Path(<str>name).stem.replace(" ", "")
self.name = f'{self.original_media_name}_{self.format_time(ms)}'
self.time = ms
self.detections = detections if detections is not None else []
for d in self.detections:
d.annotation_name = self.name
self.image = b''
def __str__(self):
if not self.detections:
return f"{self.name}: No detections"
detections_str = ", ".join(
f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})"
for d in self.detections
)
return f"{self.name}: {detections_str}"
cdef format_time(self, ms):
# Calculate hours, minutes, seconds, and hundreds of milliseconds.
h = ms // 3600000 # Total full hours.
ms_remaining = ms % 3600000
m = ms_remaining // 60000 # Full minutes.
ms_remaining %= 60000
s = ms_remaining // 1000 # Full seconds.
f = (ms_remaining % 1000) // 100 # Hundreds of milliseconds.
h = h % 10
return f"{h}{m:02}{s:02}{f}"
cdef bytes serialize(self):
return msgpack.packb({
"n": self.name,
"mn": self.original_media_name,
"i": self.image, # "i" = image
"t": self.time, # "t" = time
"d": [ # "d" = detections
{
"an": det.annotation_name,
"x": det.x,
"y": det.y,
"w": det.w,
"h": det.h,
"c": det.cls,
"p": det.confidence
} for det in self.detections
]
})
+20
View File
@@ -0,0 +1,20 @@
from user cimport User
from credentials cimport Credentials
from cdn_manager cimport CDNManager
cdef class ApiClient:
cdef Credentials credentials
cdef CDNManager cdn_manager
cdef str token, folder, api_url
cdef User user
cdef set_credentials(self, Credentials credentials)
cdef login(self)
cdef set_token(self, str token)
cdef get_user(self)
cdef load_bytes(self, str filename, str folder)
cdef upload_file(self, str filename, bytes resource, str folder)
cdef load_big_small_resource(self, str resource_name, str folder, str key)
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key)
+151
View File
@@ -0,0 +1,151 @@
import json
from http import HTTPStatus
from os import path
from uuid import UUID
import jwt
import requests
cimport constants
import yaml
from cdn_manager cimport CDNManager, CDNCredentials
from hardware_service cimport HardwareService
from security cimport Security
from user cimport User, RoleEnum
cdef class ApiClient:
"""Handles API authentication and downloading of the AI model."""
def __init__(self, str api_url):
self.credentials = None
self.user = None
self.token = None
self.cdn_manager = None
self.api_url = api_url
cdef set_credentials(self, Credentials credentials):
self.credentials = credentials
yaml_bytes = self.load_bytes(constants.CDN_CONFIG, <str>'')
yaml_config = yaml.safe_load(yaml_bytes)
creds = CDNCredentials(yaml_config["host"],
yaml_config["downloader_access_key"],
yaml_config["downloader_access_secret"],
yaml_config["uploader_access_key"],
yaml_config["uploader_access_secret"])
self.cdn_manager = CDNManager(creds)
cdef login(self):
response = requests.post(f"{self.api_url}/login",
json={"email": self.credentials.email, "password": self.credentials.password})
response.raise_for_status()
token = response.json()["token"]
self.set_token(token)
cdef set_token(self, str token):
self.token = token
claims = jwt.decode(token, options={"verify_signature": False})
try:
id = str(UUID(claims.get("nameid", "")))
except ValueError:
raise ValueError("Invalid GUID format in claims")
email = claims.get("unique_name", "")
role_str = claims.get("role", "")
if role_str == "ApiAdmin":
role = RoleEnum.ApiAdmin
elif role_str == "Admin":
role = RoleEnum.Admin
elif role_str == "ResourceUploader":
role = RoleEnum.ResourceUploader
elif role_str == "Validator":
role = RoleEnum.Validator
elif role_str == "Operator":
role = RoleEnum.Operator
else:
role = RoleEnum.NONE
self.user = User(id, email, role)
cdef get_user(self):
if self.user is None:
self.login()
return self.user
cdef upload_file(self, str filename, bytes resource, str folder):
if self.token is None:
self.login()
url = f"{self.api_url}/resources/{folder}"
headers = { "Authorization": f"Bearer {self.token}" }
files = {'data': (filename, resource)}
try:
r = requests.post(url, headers=headers, files=files, allow_redirects=True)
r.raise_for_status()
constants.log(f"Uploaded {filename} to {self.api_url}/{folder} successfully: {r.status_code}.")
except Exception as e:
constants.log(f"Upload fail: {e}")
cdef load_bytes(self, str filename, str folder):
hardware_service = HardwareService()
cdef str hardware = hardware_service.get_hardware_info()
if self.token is None:
self.login()
url = f"{self.api_url}/resources/get/{folder}"
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
payload = json.dumps(
{
"password": self.credentials.password,
"hardware": hardware,
"fileName": filename
}, indent=4)
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN:
self.login()
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
response = requests.post(url, data=payload, headers=headers, stream=True)
if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR:
print('500!')
hw_hash = Security.get_hw_hash(hardware)
key = Security.get_api_encryption_key(self.credentials, hw_hash)
resp_bytes = response.raw.read()
data = Security.decrypt_to(resp_bytes, key)
constants.log(<str>f'Downloaded file: {filename}, {len(data)} bytes')
return data
cdef load_big_small_resource(self, str resource_name, str folder, str key):
cdef str big_part = path.join(<str>folder, f'{resource_name}.big')
cdef str small_part = f'{resource_name}.small'
with open(<str>big_part, 'rb') as binary_file:
encrypted_bytes_big = binary_file.read()
encrypted_bytes_small = self.load_bytes(small_part, folder)
encrypted_bytes = encrypted_bytes_small + encrypted_bytes_big
result = Security.decrypt_to(encrypted_bytes, key)
return result
cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key):
cdef str big_part_name = f'{resource_name}.big'
cdef str small_part_name = f'{resource_name}.small'
resource_encrypted = Security.encrypt_to(<bytes>resource, key)
part_small_size = min(constants.SMALL_SIZE_KB * 1024, int(0.3 * len(resource_encrypted)))
part_small = resource_encrypted[:part_small_size] # slice bytes for part1
part_big = resource_encrypted[part_small_size:]
self.cdn_manager.upload(<str>constants.MODELS_FOLDER, <str>big_part_name, part_big)
with open(path.join(<str>folder, <str>big_part_name), 'wb') as f:
f.write(part_big)
self.upload_file(small_part_name, part_small, constants.MODELS_FOLDER)
+75
View File
@@ -0,0 +1,75 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_submodules
from PyInstaller.utils.hooks import collect_all
datas = [('venv\\Lib\\site-packages\\cv2', 'cv2')]
binaries = []
hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'cdn_manager', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'tensorrt_engine', 'onnx_engine', 'inference_engine', 'inference', 'remote_command_handler']
hiddenimports += collect_submodules('cv2')
tmp_ret = collect_all('requests')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('psutil')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('msgpack')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('zmq')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('cryptography')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('numpy')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('onnxruntime')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('tensorrt')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('pycuda')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('pynvml')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('boto3')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
tmp_ret = collect_all('jwt')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
a = Analysis(
['start.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='azaion-inference',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='azaion-inference',
)
+55
View File
@@ -0,0 +1,55 @@
echo Build Cython app
cd %~dp0
echo remove dist folder:
if exist dist rmdir dist /s /q
if exist build rmdir build /s /q
echo install python and dependencies
python -m venv venv
venv\Scripts\python -m pip install --upgrade pip
venv\Scripts\pip install -r requirements.txt
venv\Scripts\pip install --upgrade pyinstaller pyinstaller-hooks-contrib
REM Testing autobuild in Jenkins!!!Latest check
venv\Scripts\python setup.py build_ext --inplace
echo install azaion-inference
venv\Scripts\pyinstaller --name=azaion-inference ^
--collect-submodules cv2 ^
--add-data "venv\Lib\site-packages\cv2;cv2" ^
--collect-all requests ^
--collect-all psutil ^
--collect-all msgpack ^
--collect-all zmq ^
--collect-all cryptography ^
--collect-all numpy ^
--collect-all onnxruntime ^
--collect-all tensorrt ^
--collect-all pycuda ^
--collect-all pynvml ^
--collect-all boto3 ^
--collect-all jwt ^
--hidden-import constants ^
--hidden-import annotation ^
--hidden-import credentials ^
--hidden-import file_data ^
--hidden-import user ^
--hidden-import security ^
--hidden-import secure_model ^
--hidden-import cdn_manager ^
--hidden-import api_client ^
--hidden-import hardware_service ^
--hidden-import remote_command ^
--hidden-import ai_config ^
--hidden-import tensorrt_engine ^
--hidden-import onnx_engine ^
--hidden-import inference_engine ^
--hidden-import inference ^
--hidden-import remote_command_handler ^
start.py
xcopy /E dist\azaion-inference ..\dist\
copy config.production.yaml ..\dist\config.yaml
+14
View File
@@ -0,0 +1,14 @@
cdef class CDNCredentials:
cdef str host
cdef str downloader_access_key
cdef str downloader_access_secret
cdef str uploader_access_key
cdef str uploader_access_secret
cdef class CDNManager:
cdef CDNCredentials creds
cdef object download_client
cdef object upload_client
cdef upload(self, str bucket, str filename, bytes file_bytes)
cdef download(self, str bucket, str filename)
+41
View File
@@ -0,0 +1,41 @@
import io
import boto3
cdef class CDNCredentials:
def __init__(self, host, downloader_access_key, downloader_access_secret, uploader_access_key, uploader_access_secret):
self.host = host
self.downloader_access_key = downloader_access_key
self.downloader_access_secret = downloader_access_secret
self.uploader_access_key = uploader_access_key
self.uploader_access_secret = uploader_access_secret
cdef class CDNManager:
def __init__(self, CDNCredentials credentials):
self.creds = credentials
self.download_client = boto3.client('s3', endpoint_url=self.creds.host,
aws_access_key_id=self.creds.downloader_access_key,
aws_secret_access_key=self.creds.downloader_access_secret)
self.upload_client = boto3.client('s3', endpoint_url=self.creds.host,
aws_access_key_id=self.creds.uploader_access_key,
aws_secret_access_key=self.creds.uploader_access_secret)
cdef upload(self, str bucket, str filename, bytes file_bytes):
try:
self.upload_client.upload_fileobj(io.BytesIO(file_bytes), bucket, filename)
print(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}')
return True
except Exception as e:
print(e)
return False
cdef download(self, str bucket, str filename):
try:
self.download_client.download_file(bucket, filename, filename)
print(f'downloaded {filename} from the {bucket} to current folder')
return True
except Exception as e:
print(e)
return False
+19
View File
@@ -0,0 +1,19 @@
cdef str CONFIG_FILE # Port for the zmq
cdef int QUEUE_MAXSIZE # Maximum size of the command queue
cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit
cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit
cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api
cdef str AI_ONNX_MODEL_FILE
cdef str CDN_CONFIG
cdef str MODELS_FOLDER
cdef int SMALL_SIZE_KB
cdef bytes DONE_SIGNAL
cdef log(str log_message, bytes client_id=*)
+21
View File
@@ -0,0 +1,21 @@
import time
cdef str CONFIG_FILE = "config.yaml" # Port for the zmq
cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue
cdef str COMMANDS_QUEUE = "azaion-commands"
cdef str ANNOTATIONS_QUEUE = "azaion-annotations"
cdef str QUEUE_CONFIG_FILENAME = "secured-config.json"
cdef str AI_ONNX_MODEL_FILE = "azaion.onnx"
cdef str CDN_CONFIG = "cdn.yaml"
cdef str MODELS_FOLDER = "models"
cdef int SMALL_SIZE_KB = 3
cdef log(str log_message, bytes client_id=None):
local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
client_str = '' if client_id is None else f' {client_id}'
print(f'[{local_time}{client_str}]: {log_message}')
+6
View File
@@ -0,0 +1,6 @@
cdef class Credentials:
cdef public str email
cdef public str password
@staticmethod
cdef from_msgpack(bytes data)
+15
View File
@@ -0,0 +1,15 @@
from msgpack import unpackb
cdef class Credentials:
def __init__(self, str email, str password):
self.email = email
self.password = password
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return Credentials(
unpacked.get("Email"),
unpacked.get("Password"))
+6
View File
@@ -0,0 +1,6 @@
cdef class FileData:
cdef public str folder
cdef public str filename
@staticmethod
cdef from_msgpack(bytes data)
+14
View File
@@ -0,0 +1,14 @@
from msgpack import unpackb
cdef class FileData:
def __init__(self, str folder, str filename):
self.folder = folder
self.filename = filename
@staticmethod
cdef from_msgpack(bytes data):
unpacked = unpackb(data, strict_map_key=False)
return FileData(
unpacked.get("Folder"),
unpacked.get("Filename"))
+6
View File
@@ -0,0 +1,6 @@
cdef class HardwareService:
cdef bint is_windows
@staticmethod
cdef has_nvidia_gpu()
cdef str get_hardware_info(self)
+75
View File
@@ -0,0 +1,75 @@
import subprocess
import pynvml
cdef class HardwareService:
"""Handles hardware information retrieval and hash generation."""
def __init__(self):
try:
res = subprocess.check_output("ver", shell=True).decode('utf-8')
if "Microsoft Windows" in res:
self.is_windows = True
else:
self.is_windows = False
except Exception:
print('Error during os type checking')
self.is_windows = False
@staticmethod
cdef has_nvidia_gpu():
try:
pynvml.nvmlInit()
device_count = pynvml.nvmlDeviceGetCount()
if device_count > 0:
print(f"Found NVIDIA GPU(s).")
return True
else:
print("No NVIDIA GPUs found by NVML.")
return False
except pynvml.NVMLError as error:
print(f"Failed to find NVIDIA GPU")
return False
finally:
try:
pynvml.nvmlShutdown()
except:
print('Failed to shutdown pynvml cause probably no NVidia GPU')
pass
cdef str get_hardware_info(self):
if self.is_windows:
os_command = (
"powershell -Command \""
"Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; "
"Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output; "
"(Get-Disk | Where-Object {$_.IsSystem -eq $true}).SerialNumber"
"\""
)
else:
os_command = (
"/bin/bash -c \" lscpu | grep 'Model name:' | cut -d':' -f2 && "
"lspci | grep VGA | cut -d':' -f3 && "
"free -g | grep Mem: | awk '{print $2}' && \""
"udevadm info --query=property --name=\"/dev/$(lsblk -no pkname \"$(findmnt -n -o SOURCE --target /)\")\" | grep -E 'ID_SERIAL=|ID_SERIAL_SHORT=' | cut -d'=' -f2- | head -n1 && "
)
# in case of subprocess error do:
# cdef bytes os_command_bytes = os_command.encode('utf-8')
# and use os_command_bytes
result = subprocess.check_output(os_command, shell=True).decode('utf-8')
lines = [line.strip() for line in result.splitlines() if line.strip()]
cdef str cpu = lines[0].replace("Name=", "").replace(" ", " ")
cdef str gpu = lines[1].replace("Name=", "").replace(" ", " ")
# could be multiple gpus
len_lines = len(lines)
cdef str memory = lines[len_lines-2].replace("TotalVisibleMemorySize=", "").replace(" ", " ")
cdef str drive_serial = lines[len_lines-1]
cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}'
return res

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