361 Commits

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

TODO: Load dlls separately by Loader UI and loader client

WIP
2025-06-06 20:04:03 +03:00
Alex Bezdieniezhnykh 500db31142 add azaion loader 2025-06-01 19:16:49 +03:00
Alex Bezdieniezhnykh 2584b4f125 hardware service works on linux 2025-05-31 17:48:26 +03:00
Alex Bezdieniezhnykh 42867560c3 fix checking api client to download newest version 2025-05-30 17:18:55 +03:00
Alex Bezdieniezhnykh b5f2a75b86 bump up
version
2025-05-30 11:24:17 +03:00
Alex Bezdieniezhnykh 1b6c440dcc fix re-send new batch to gps denied
todo: clear folders, consider better center point to fetch next batch from satellite provider
2025-05-30 11:03:00 +03:00
Alex Bezdieniezhnykh b345137f16 fix bugs with UI for gps denied 2025-05-30 02:09:15 +03:00
Alex Bezdieniezhnykh d87ddb5f6a cleanup 2025-05-29 00:36:40 +03:00
Alex Bezdieniezhnykh d842466594 gps matcher async
put cryptography lib to fixed version
fix race condition bug in queue handler
add lock to db writing and backup to file db on each write
2025-05-29 00:35:35 +03:00
Alex Bezdieniezhnykh 34ea821fb3 better view for class distribution 2025-05-27 13:26:37 +03:00
dzaitsev e957f1192a updatedd zip_full.ps1 top pick only the latest file by LastWriteTime 2025-05-23 12:11:48 +03:00
dzaitsev d90b2750e0 Merge remote-tracking branch 'origin/dev' into dev 2025-05-22 10:15:59 +03:00
dzaitsev cea8083422 updated gdrive_retention.ps1 2025-05-22 10:15:40 +03:00
Alex Bezdieniezhnykh 757fb1e454 add more information to annotationbulkmessage 2025-05-22 00:33:13 +03:00
dzaitsev 0d51f3c373 updated gdrive_retention.ps1 2025-05-21 21:30:54 +03:00
dzaitsev 176a3ed0a6 updated gdrive_upload_full.ps1 2025-05-21 20:52:42 +03:00
dzaitsev a2e4157b9f updated gdrive_upload_iterative.ps1 2025-05-21 20:46:37 +03:00
dzaitsev 080d98389d Revert "fixed workdir path"
This reverts commit d35b97fdec.
2025-05-21 20:08:44 +03:00
dzaitsev d35b97fdec fixed workdir path 2025-05-21 19:34:56 +03:00
dzaitsev 541429aabd added retention script for gdrive 2025-05-21 18:42:55 +03:00
dzaitsev 39ed74996d updated zip and gdrive upload files. 2025-05-21 18:28:20 +03:00
dzaitsev f870198011 Merge remote-tracking branch 'origin/dev' into dev 2025-05-21 14:13:06 +03:00
dzaitsev 12aee103df Merge remote-tracking branch 'origin/pipeline' into dev 2025-05-21 14:08:40 +03:00
dzaitsev 6fce10eb39 added zip.bat to pack build artefacts for both Full and Iterative installers. 2025-05-21 14:05:41 +03:00
Alex Bezdieniezhnykh 522af51a8d don't send image to the queue on editing 2025-05-20 12:31:18 +03:00
Alex Bezdieniezhnykh edd803c304 fix classes colors, add caponier 2025-05-20 12:03:52 +03:00
Alex Bezdieniezhnykh a5fcb0988b fixed sorting in datasetexplorer, also show date
make annotationstatus more clear
2025-05-20 11:02:24 +03:00
Alex Bezdieniezhnykh 66bfe474c2 fix security 2 2025-05-18 20:31:43 +03:00
Alex Bezdieniezhnykh f2b57dccc0 security fix 2025-05-18 20:14:19 +03:00
Alex Bezdieniezhnykh c5e81ebcc6 fixed bugs with queue handling. At least most of them 2025-05-18 20:11:19 +03:00
Alex Bezdieniezhnykh cf563571c8 log queue errors 2025-05-17 19:38:07 +03:00
Alex Bezdieniezhnykh dae342b70e visual fixes 2025-05-17 19:34:02 +03:00
Alex Bezdieniezhnykh d02550f5a0 huge queue refactoring:
3 queues -> 1 queue
send delete validate updates
2025-05-17 19:25:33 +03:00
Alex Bezdieniezhnykh 87ceaa805b prepare to build 2025-05-14 19:53:35 +03:00
Alex Bezdieniezhnykh f41d49bd26 Merge remote-tracking branch 'origin/pipeline' into dev 2025-05-14 13:01:13 +03:00
Alex Bezdieniezhnykh 58cd0b06e9 Merge remote-tracking branch 'origin/dev' into dev 2025-05-14 12:45:03 +03:00
dzaitsev d92da6afa4 Errors sending to UI
notifying client of AI model conversion
2025-05-14 12:43:50 +03:00
dzaitsev 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 85f4d47318 Merge remote-tracking branch 'origin/dev' into dev 2025-05-07 17:32:29 +03:00
dzaitsev 006499c2ee fixed pushd / pulld commands in cmd 2025-05-07 17:31:16 +03:00
Alex Bezdieniezhnykh 568901edb6 Merge remote-tracking branch 'origin/dev' into dev 2025-05-07 17:14:37 +03:00
dzaitsev 8bf9d1d7e2 altered pushd / pulld commands in cmd 2025-05-07 16:47:26 +03:00
Alex Bezdieniezhnykh 79770dc7fa fix installer 2025-05-07 16:31:39 +03:00
dzaitsev 22f047bbe5 changed robocopy return code 2025-05-07 16:18:38 +03:00
dzaitsev 52c724eac6 changed robocopy return code 2025-05-07 16:15:02 +03:00
dzaitsev 8d9fe62c53 changed robocopy return code 2025-05-07 16:12:06 +03:00
dzaitsev 41b96e2ce5 Revert "init.cmd - fix for Jenkins incorrect handle of pushd/popd"
This reverts commit b8b4a33f9f.
2025-05-07 15:58:48 +03:00
dzaitsev b8b4a33f9f init.cmd - fix for Jenkins incorrect handle of pushd/popd 2025-05-07 15:12:52 +03:00
Alex Bezdieniezhnykh b937ed8051 Separate installers to Full and Iterative 2025-05-07 12:29:43 +03:00
Alex Bezdieniezhnykh f49c4e9d37 show created first 2025-05-05 09:50:49 +03:00
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
Deen 7ebe600adf Update requirements.txt
opencv added version explicitly
2025-04-30 12:01:38 +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
263 changed files with 11948 additions and 2854 deletions
+17
View File
@@ -1,8 +1,25 @@
.idea .idea
bin bin
obj obj
*.dll
*.exe
*.log
.vs .vs
*.DotSettings* *.DotSettings*
*.user *.user
log*.txt log*.txt
secured-config secured-config
build
venv
*.c
*.pyd
cython_debug*
dist-dlls
dist-azaion
Azaion*.exe
Azaion*.bin
azaion\.*\.big
_internal
dist
*.jpg
+33
View File
@@ -0,0 +1,33 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Azaion.Suite (with credentials)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
"args": ["credsManual", "-e", "test-admin@azaion.com", "-p", "Az@1on1000TestT-addminn11"],
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Launch Azaion.Suite",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows/Azaion.Suite.dll",
"args": [],
"cwd": "${workspaceFolder}/Azaion.Suite/bin/Debug/net8.0-windows",
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": "Attach to Azaion.Suite",
"type": "coreclr",
"request": "attach",
"processName": "Azaion.Suite"
}
]
}
+41
View File
@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Azaion.Suite.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Azaion.Suite.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Azaion.Suite.sln"
],
"problemMatcher": "$msCompile"
}
]
}
+152 -33
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
@@ -70,6 +76,7 @@
<RowDefinition Height="28"></RowDefinition> <RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="28"></RowDefinition> <RowDefinition Height="28"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -148,14 +155,22 @@
Grid.Column="0" Grid.Column="0"
Name="LvFiles" Name="LvFiles"
Background="Black" Background="Black"
SelectedItem="{Binding Path=SelectedVideo}" Foreground="#FFA4AFCC" SelectedItem="{Binding Path=SelectedVideo}"
Foreground="#FFDDDDDD"
> >
<ListView.Resources> <ListView.Resources>
<Style TargetType="{x:Type ListViewItem}"> <Style TargetType="{x:Type ListViewItem}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding HasAnnotations}" Value="true"> <DataTrigger Binding="{Binding HasAnnotations}" Value="true">
<Setter Property="Background" Value="Gray"/> <Setter Property="Background" Value="#FF505050"/>
</DataTrigger> </DataTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value=" DimGray" />
<Setter Property="Background" Value="#FFCCCCCC"></Setter>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Foreground" Value="DimGray"></Setter>
</Trigger>
</Style.Triggers> </Style.Triggers>
<EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter> <EventSetter Event="ContextMenuOpening" Handler="LvFilesContextOpening"></EventSetter>
</Style> </Style>
@@ -163,6 +178,7 @@
<ListView.ContextMenu> <ListView.ContextMenu>
<ContextMenu Name="LvFilesContextMenu"> <ContextMenu Name="LvFilesContextMenu">
<MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" /> <MenuItem Header="Відкрити папку..." Click="OpenContainingFolder" Background="WhiteSmoke" />
<MenuItem Header="Видалити..." Click="DeleteMedia" Background="WhiteSmoke" />
</ContextMenu> </ContextMenu>
</ListView.ContextMenu> </ListView.ContextMenu>
<ListView.View> <ListView.View>
@@ -176,10 +192,20 @@
</GridView> </GridView>
</ListView.View> </ListView.View>
</ListView> </ListView>
<controls1:CameraConfigControl
x:Name="CameraConfigControl"
Grid.Column="0"
Grid.Row="4"
Camera="{Binding Camera, RelativeSource={RelativeSource AncestorType=Window}, Mode=OneWay}"
>
</controls1:CameraConfigControl>
<controls1:DetectionClasses <controls1:DetectionClasses
x:Name="LvClasses" x:Name="LvClasses"
Grid.Column="0" Grid.Column="0"
Grid.Row="4"> Grid.Row="5">
</controls1:DetectionClasses> </controls1:DetectionClasses>
<GridSplitter <GridSplitter
@@ -187,7 +213,7 @@
ResizeDirection="Columns" ResizeDirection="Columns"
Grid.Column="1" Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="4" Grid.RowSpan="5"
ResizeBehavior="PreviousAndNext" ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -196,7 +222,7 @@
<wpf:VideoView <wpf:VideoView
Grid.Row="1" Grid.Row="1"
Grid.Column="2" Grid.Column="2"
Grid.RowSpan="4" Grid.RowSpan="5"
x:Name="VideoView"> x:Name="VideoView">
<controls1:CanvasEditor x:Name="Editor" <controls1:CanvasEditor x:Name="Editor"
Background="#01000000" Background="#01000000"
@@ -209,7 +235,7 @@
ResizeDirection="Columns" ResizeDirection="Columns"
Grid.Column="3" Grid.Column="3"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="4" Grid.RowSpan="5"
ResizeBehavior="PreviousAndNext" ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -219,9 +245,8 @@
<DataGrid x:Name="DgAnnotations" <DataGrid x:Name="DgAnnotations"
Grid.Column="4" Grid.Column="4"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="4" Grid.RowSpan="5"
Background="Black" Background="Black"
RowBackground="#252525"
Foreground="White" Foreground="White"
RowHeaderWidth="0" RowHeaderWidth="0"
Padding="2 0 0 0" Padding="2 0 0 0"
@@ -230,7 +255,8 @@
CellStyle="{DynamicResource DataGridCellStyle1}" CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
CanUserResizeColumns="False"> CanUserResizeColumns="False"
RowStyleSelector="{StaticResource GradientStyleSelector}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn <DataGridTextColumn
Width="60" Width="60"
@@ -253,28 +279,30 @@
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"></Setter>
</Style> </Style>
</DataGridTextColumn.HeaderStyle> </DataGridTextColumn.HeaderStyle>
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0 " EndPoint="1 0">
<GradientStop Offset="0.3" Color="{Binding Path=ClassColor0}" />
<GradientStop Offset="0.5" Color="{Binding Path=ClassColor1}" />
<GradientStop Offset="0.8" Color="{Binding Path=ClassColor2}" />
<GradientStop Offset="0.99" Color="{Binding Path=ClassColor3}" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</Grid> </Grid>
<GridSplitter
Name="GpsSplitter"
Background="DarkGray"
ResizeDirection="Rows"
Grid.Row="1"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="Collapsed" />
<controls:MapMatcher
x:Name="MapMatcherComponent"
Grid.Column="0"
Grid.Row="2"
/>
<controls2:UpdatableProgressBar x:Name="VideoSlider" <controls2:UpdatableProgressBar x:Name="VideoSlider"
Grid.Column="0" Grid.Column="0"
Grid.Row="1" Grid.Row="3"
Background="#252525" Background="#252525"
Foreground="LightBlue"> Foreground="LightBlue">
</controls2:UpdatableProgressBar> </controls2:UpdatableProgressBar>
@@ -282,7 +310,7 @@
<!-- Buttons --> <!-- Buttons -->
<Grid <Grid
Name="Buttons" Name="Buttons"
Grid.Row="2" Grid.Row="4"
Background="Black" Background="Black"
> >
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -297,7 +325,10 @@
<ColumnDefinition Width="28" /> <!-- 8 --> <ColumnDefinition Width="28" /> <!-- 8 -->
<ColumnDefinition Width="56" /> <!-- 9 --> <ColumnDefinition Width="56" /> <!-- 9 -->
<ColumnDefinition Width="28" /> <!-- 10 --> <ColumnDefinition Width="28" /> <!-- 10 -->
<ColumnDefinition Width="*" /> <!-- 11 --> <ColumnDefinition Width="28" /> <!-- 11 -->
<ColumnDefinition Width="28" /> <!-- 12 -->
<ColumnDefinition Width="28" /> <!-- 13 -->
<ColumnDefinition Width="*" /> <!-- 14-->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black" <Button Grid.Column="0" Padding="5" ToolTip="Включити програвання" Background="Black" BorderBrush="Black"
Click="PlayClick"> Click="PlayClick">
@@ -477,11 +508,12 @@
<Button <Button
x:Name="AIDetectBtn" x:Name="AIDetectBtn"
IsEnabled="False"
Grid.Column="10" Grid.Column="10"
Padding="2" Width="25" Padding="2" Width="25"
Height="25" Height="25"
ToolTip="Розпізнати за допомогою AI. Клавіша: [A]" Background="Black" BorderBrush="Black" ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="AutoDetect"> Click="AIDetectBtn_OnClick">
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477 <Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4 39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139 0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
@@ -508,8 +540,95 @@
21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053 21.333 5.494 5.493 13.058 8.907 21.343 8.907h223.368c8.273 0 15.833-3.421 21.326-8.914s8.915-13.053
8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" /> 8.915-21.326V141.338c0-8.283-3.414-15.848-8.908-21.341v-.049c-5.454-5.456-13.006-8.851-21.333-8.851z" />
</Button> </Button>
<Button Grid.Column="11" Padding="2" Width="25" Height="25" ToolTip="Показати GPS. Клавіша: [M]" Background="Black" BorderBrush="Black"
Click="SwitchGpsPanel">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V520 H580 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="M307.1,311.97c-12.55-14-22.75-31.86-32.9-47.68c-10.23-15.94-19.78-32.43-27.3-49.83c-7.03-16.28-12.48-33.08-9.25-50.97
c2.87-15.93,11.75-31.29,23.84-42.03c22.3-19.8,57.81-22.55,82.67-5.98c29.17,19.45,39.48,55.06,27.59,87.55
c-6.8,18.59-16.41,36.14-27.02,52.8C332.76,274.63,320.84,294.45,307.1,311.97z M307.01,143.45c-38.65-0.46-39.68,59.79-0.95,60.47
c16.47,0.29,30.83-13.34,31-29.9C337.22,157.75,323.24,143.65,307.01,143.45z" />
<GeometryDrawing Brush="LightGray" Geometry="M367.34,310.68c10.09,2.5,23.61,4.83,31.46,12.19c11.05,10.35-5.42,18.17-14.21,21.43c-24.55,9.11-53.52,10.41-79.44,10.11
c-25.7-0.3-54.62-1.23-78.68-11.19c-7.68-3.18-21.53-10.2-12.52-19.47c8.26-8.49,23.33-11.42,34.5-12.94
c-5.15,1.98-16.18,5.12-17.07,11.49c-1,7.13,9.78,10.81,15.02,12.59c18.28,6.22,38.72,7.58,57.89,7.73
c18.91,0.15,38.85-0.72,57.13-5.92c5.72-1.63,18.65-4.74,20.7-11.49c2.28-7.47-9.8-11.66-15.04-13.71
C367.18,311.22,367.26,310.95,367.34,310.68z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<Button Grid.Column="12"
Padding="2"
Width="25"
Height="25"
ToolTip="Показати обєкти по аудіоаналізу. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="SoundDetections">
<Image>
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup>
<GeometryDrawing Geometry="m19.05,171.43v152.38">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m95.24,95.24v342.86">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m171.43,209.52v76.19">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m247.62,133.33v259.58">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<StatusBar Grid.Column="11" <GeometryDrawing Geometry="m323.81,19.05v457.14">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m401.43,86.69v342.86">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
<GeometryDrawing Geometry="m473.43,209.02v76.19">
<GeometryDrawing.Pen>
<Pen Brush="LightGray" Thickness="38.1" StartLineCap="Round" EndLineCap="Round" LineJoin="Round" />
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Button>
<Button Grid.Column="13"
Padding="2"
Width="25"
Height="25"
ToolTip="Аналіз стану БПЛА. Клавіша: [K]" Background="Black" BorderBrush="Black"
Click="RunDroneMaintenance">
<Path Stretch="Fill" Fill="LightGray" Data="
M128,7.10542736e-15 C198.692448,7.10542736e-15 256,57.307552 256,128 C256,140.931179 254.082471,153.414494 250.516246,165.181113 L384,298.666667
C407.564149,322.230816 407.564149,360.435851 384,384 C360.435851,407.564149 322.230816,407.564149 298.666667,384 L165.181113,250.516246
C153.414494,254.082471 140.931179,256 128,256 C57.307552,256 7.10542736e-15,198.692448 7.10542736e-15,128 C7.10542736e-15,114.357909
2.13416363,101.214278 6.08683609,88.884763 L66.6347809,149.333333 L126.649,129.346 L129.329,126.666 L149.333333,66.7080586 L88.7145729,6.14152881
C101.0933,2.15385405 114.29512,7.10542736e-15 128,7.10542736e-15 Z" />
</Button>
<StatusBar Grid.Column="14"
Background="#252525" Background="#252525"
Foreground="White"> Foreground="White">
<StatusBar.ItemsPanel> <StatusBar.ItemsPanel>
File diff suppressed because it is too large Load Diff
+298 -71
View File
@@ -1,12 +1,21 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Azaion.Annotator.Controls;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.Common.Services.Inference;
using GMap.NET;
using GMap.NET.WindowsPresentation;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -16,22 +25,32 @@ using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
namespace Azaion.Annotator; namespace Azaion.Annotator;
public class AnnotatorEventHandler( public class AnnotatorEventHandler(
LibVLC libVLC, LibVLC libVlc,
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
Annotator mainWindow, Annotator mainWindow,
FormState formState, FormState formState,
AnnotationService annotationService, IAnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger, ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig) IOptions<DirectoriesConfig> dirConfig,
IOptions<AnnotationConfig> annotationConfig,
IInferenceService inferenceService,
IDbFactory dbFactory,
IAzaionApi api,
FailsafeAnnotationsProducer producer)
: :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<PlaybackControlEvent>, INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent> INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent>,
INotificationHandler<AnnotationAddedEvent>,
INotificationHandler<SetStatusTextEvent>,
INotificationHandler<GPSMatcherResultProcessedEvent>,
INotificationHandler<AIAvailabilityStatusEvent>
{ {
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
private const int RESULT_WIDTH = 1280; private readonly string _tempImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, "___temp___.jpg");
private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new() private readonly Dictionary<Key, PlaybackControlEnum> _keysControlEnumDict = new()
{ {
@@ -45,21 +64,21 @@ public class AnnotatorEventHandler(
{ Key.PageDown, PlaybackControlEnum.Next }, { Key.PageDown, PlaybackControlEnum.Next },
}; };
public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct)
{ {
SelectClass(notification.DetectionClass); SelectClass(notification.DetectionClass);
await Task.CompletedTask; await Task.CompletedTask;
} }
private void SelectClass(DetectionClass annClass) private void SelectClass(DetectionClass detClass)
{ {
mainWindow.Editor.CurrentAnnClass = annClass; mainWindow.Editor.CurrentAnnClass = detClass;
foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected)) foreach (var ann in mainWindow.Editor.CurrentDetections.Where(x => x.IsSelected))
ann.DetectionClass = annClass; ann.DetectionClass = detClass;
mainWindow.LvClasses.SelectedIndex = annClass.Id; mainWindow.LvClasses.SelectNum(detClass.Id);
} }
public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default) public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default)
{ {
if (keyEvent.WindowEnum != WindowEnum.Annotator) if (keyEvent.WindowEnum != WindowEnum.Annotator)
return; return;
@@ -72,22 +91,22 @@ public class AnnotatorEventHandler(
if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9) if ((int)key >= (int)Key.NumPad1 && (int)key <= (int)Key.NumPad9)
keyNumber = key - Key.NumPad1; keyNumber = key - Key.NumPad1;
if (keyNumber.HasValue) if (keyNumber.HasValue)
SelectClass((DetectionClass)mainWindow.LvClasses.Items[keyNumber.Value]!); SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!);
if (_keysControlEnumDict.TryGetValue(key, out var value)) if (_keysControlEnumDict.TryGetValue(key, out var value))
await ControlPlayback(value, cancellationToken); await ControlPlayback(value, ct);
if (key == Key.A) if (key == Key.R)
mainWindow.AutoDetect(null!, null!); await mainWindow.AutoDetect();
#region Volume #region Volume
switch (key) switch (key)
{ {
case Key.VolumeMute when mediaPlayer.Volume == 0: case Key.VolumeMute when mediaPlayer.Volume == 0:
await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken); await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct);
break; break;
case Key.VolumeMute: case Key.VolumeMute:
await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken); await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct);
break; break;
case Key.Up: case Key.Up:
case Key.VolumeUp: case Key.VolumeUp:
@@ -105,9 +124,9 @@ public class AnnotatorEventHandler(
#endregion #endregion
} }
public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken = default) public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default)
{ {
await ControlPlayback(notification.PlaybackControl, cancellationToken); await ControlPlayback(notification.PlaybackControl, ct);
mainWindow.VideoView.Focus(); mainWindow.VideoView.Focus();
} }
@@ -121,19 +140,29 @@ public class AnnotatorEventHandler(
switch (controlEnum) switch (controlEnum)
{ {
case PlaybackControlEnum.Play: case PlaybackControlEnum.Play:
Play(); await Play(cancellationToken);
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
if (mediaPlayer.IsPlaying)
{
mediaPlayer.Pause(); mediaPlayer.Pause();
if (!mediaPlayer.IsPlaying) mediaPlayer.TakeSnapshot(0, _tempImgPath, 0, 0);
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]); mainWindow.Editor.SetBackground(await _tempImgPath.OpenImage());
formState.BackgroundTime = TimeSpan.FromMilliseconds(mediaPlayer.Time);
}
else
{
mediaPlayer.Play();
if (formState.BackgroundTime.HasValue) if (formState.BackgroundTime.HasValue)
{ {
mainWindow.Editor.ResetBackground(); mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null; formState.BackgroundTime = null;
} }
}
break; break;
case PlaybackControlEnum.Stop: case PlaybackControlEnum.Stop:
inferenceService.StopInference();
await mainWindow.DetCancelSource.CancelAsync();
mediaPlayer.Stop(); mediaPlayer.Stop();
break; break;
case PlaybackControlEnum.PreviousFrame: case PlaybackControlEnum.PreviousFrame:
@@ -143,10 +172,17 @@ public class AnnotatorEventHandler(
mainWindow.SeekTo(mediaPlayer.Time + step); mainWindow.SeekTo(mediaPlayer.Time + step);
break; break;
case PlaybackControlEnum.SaveAnnotations: case PlaybackControlEnum.SaveAnnotations:
await SaveAnnotations(cancellationToken); await SaveAnnotation(cancellationToken);
break; break;
case PlaybackControlEnum.RemoveSelectedAnns: case PlaybackControlEnum.RemoveSelectedAnns:
var focusedElement = FocusManager.GetFocusedElement(mainWindow);
if (focusedElement is ListViewItem item)
{
if (item.DataContext is not MediaFileInfo mediaFileInfo)
return;
mainWindow.DeleteMedia(mediaFileInfo);
}
else
mainWindow.Editor.RemoveSelectedAnns(); mainWindow.Editor.RemoveSelectedAnns();
break; break;
case PlaybackControlEnum.RemoveAllAnns: case PlaybackControlEnum.RemoveAllAnns:
@@ -164,10 +200,10 @@ public class AnnotatorEventHandler(
mediaPlayer.Volume = 0; mediaPlayer.Volume = 0;
break; break;
case PlaybackControlEnum.Previous: case PlaybackControlEnum.Previous:
NextMedia(isPrevious: true); await NextMedia(isPrevious: true, ct: cancellationToken);
break; break;
case PlaybackControlEnum.Next: case PlaybackControlEnum.Next:
NextMedia(); await NextMedia(ct: cancellationToken);
break; break;
case PlaybackControlEnum.None: case PlaybackControlEnum.None:
break; break;
@@ -182,7 +218,7 @@ public class AnnotatorEventHandler(
} }
} }
private void NextMedia(bool isPrevious = false) private async Task NextMedia(bool isPrevious = false, CancellationToken ct = default)
{ {
var increment = isPrevious ? -1 : 1; var increment = isPrevious ? -1 : 1;
var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count; var check = isPrevious ? -1 : mainWindow.LvFiles.Items.Count;
@@ -190,10 +226,10 @@ public class AnnotatorEventHandler(
return; return;
mainWindow.LvFiles.SelectedIndex += increment; mainWindow.LvFiles.SelectedIndex += increment;
Play(); await Play(ct);
} }
public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) public async Task Handle(VolumeChangedEvent notification, CancellationToken ct)
{ {
ChangeVolume(notification.Volume); ChangeVolume(notification.Volume);
await Task.CompletedTask; await Task.CompletedTask;
@@ -205,68 +241,259 @@ public class AnnotatorEventHandler(
mediaPlayer.Volume = volume; mediaPlayer.Volume = volume;
} }
private void Play() private async Task Play(CancellationToken ct = default)
{ {
if (mainWindow.LvFiles.SelectedItem == null) if (mainWindow.LvFiles.SelectedItem == null)
return; return;
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem; var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
mainWindow.Editor.ResetBackground();
if (formState.CurrentMedia == mediaInfo)
return; //already loaded
formState.CurrentMedia = mediaInfo; formState.CurrentMedia = mediaInfo;
mainWindow.Title = $"{mainWindow.MainTitle} - {mediaInfo.Name}";
await mainWindow.ReloadAnnotations();
if (mediaInfo.MediaType == MediaTypes.Video)
{
mainWindow.Editor.SetBackground(null);
//need to wait a bit for correct VLC playback event handling
await Task.Delay(100, ct);
mediaPlayer.Stop(); mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; mediaPlayer.Play(new Media(libVlc, mediaInfo.Path));
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]); }
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); else
{
formState.BackgroundTime = TimeSpan.Zero;
var image = await mediaInfo.Path.OpenImage();
formState.CurrentMediaSize = new Size(image.PixelWidth, image.PixelHeight);
mainWindow.Editor.SetBackground(image);
mediaPlayer.Stop();
mainWindow.ShowTimeAnnotations(TimeSpan.Zero, showImage: true);
}
} }
//SAVE: MANUAL //SAVE: MANUAL
private async Task SaveAnnotations(CancellationToken cancellationToken = default) private async Task SaveAnnotation(CancellationToken cancellationToken = default)
{ {
if (formState.CurrentMedia == null) if (formState.CurrentMedia == null)
return; return;
var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time); var time = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var fName = formState.GetTimeName(time); var timeName = formState.MediaName.ToTimeName(time);
var currentDetections = mainWindow.Editor.CurrentDetections
.Select(x => new Detection(fName, x.GetLabel(mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)))
.ToList();
await mainWindow.AddAnnotations(fName, currentDetections, cancellationToken);
formState.CurrentMedia.HasAnnotations = mainWindow.Detections.Count != 0;
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video;
var imageExtension = isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path); var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{timeName}{Constants.JPG_EXT}");
var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{imageExtension}");
formState.CurrentMedia.HasAnnotations = mainWindow.Editor.CurrentDetections.Count != 0;
var annotations = await SaveAnnotationInner(imgPath, cancellationToken);
if (isVideo) if (isVideo)
{ {
if (formState.BackgroundTime.HasValue) foreach (var annotation in annotations)
{ mainWindow.AddAnnotation(annotation);
//no need to save image, it's already there, just remove background
mainWindow.Editor.ResetBackground();
formState.BackgroundTime = null;
//next item
var annGrid = mainWindow.DgAnnotations;
annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
}
else
{
var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height);
mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight);
mediaPlayer.Play(); mediaPlayer.Play();
}
// next item. Probably not needed
// var annGrid = mainWindow.DgAnnotations;
// annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1);
// mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem);
mainWindow.Editor.SetBackground(null);
formState.BackgroundTime = null;
} }
else else
{ {
File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true); await NextMedia(ct: cancellationToken);
NextMedia();
} }
await annotationService.SaveAnnotation(fName, imageExtension, currentDetections, SourceEnum.Manual, token: cancellationToken);
mainWindow.LvFiles.Items.Refresh();
mainWindow.Editor.RemoveAllAnns();
}
private async Task<List<Annotation>> SaveAnnotationInner(string imgPath, CancellationToken cancellationToken = default)
{
var canvasDetections = mainWindow.Editor.CurrentDetections.Select(x => x.ToCanvasLabel()).ToList();
var source = (mainWindow.Editor.BackgroundImage.Source as BitmapSource)!;
var mediaSize = new Size(source.PixelWidth, source.PixelHeight);
var annotationsResult = new List<Annotation>();
if (!File.Exists(imgPath))
{
if (mediaSize.FitSizeForAI())
await source.SaveImage(imgPath, cancellationToken);
else
{
//Tiling
//1. Convert from RenderSize to CurrentMediaSize
var detectionCoords = canvasDetections.Select(x => new CanvasLabel(
new YoloLabel(x, mainWindow.Editor.RenderSize, formState.CurrentMediaSize), formState.CurrentMediaSize, null, x.Confidence))
.ToList();
//2. Split to frames
var results = TileProcessor.Split(formState.CurrentMediaSize, detectionCoords, cancellationToken);
//3. Save each frame as a separate annotation
foreach (var res in results)
{
var time = TimeSpan.Zero;
var annotationName = $"{formState.MediaName}{Constants.SPLIT_SUFFIX}{res.Tile.Width}_{res.Tile.Left:0000}_{res.Tile.Top:0000}!".ToTimeName(time);
var tileImgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{annotationName}{Constants.JPG_EXT}");
var bitmap = new CroppedBitmap(source, new Int32Rect((int)res.Tile.Left, (int)res.Tile.Top, (int)res.Tile.Width, (int)res.Tile.Height));
await bitmap.SaveImage(tileImgPath, cancellationToken);
var frameSize = new Size(res.Tile.Width, res.Tile.Height);
var detections = res.Detections
.Select(det => det.ReframeToSmall(res.Tile))
.Select(x => new Detection(annotationName, new YoloLabel(x, frameSize)))
.ToList();
annotationsResult.Add(await annotationService.SaveAnnotation(formState.MediaName, annotationName, time, detections, token: cancellationToken));
}
return annotationsResult;
}
}
var timeImg = formState.BackgroundTime ?? TimeSpan.FromMilliseconds(mediaPlayer.Time);
var annName = formState.MediaName.ToTimeName(timeImg);
var currentDetections = canvasDetections.Select(x =>
new Detection(annName, new YoloLabel(x, mainWindow.Editor.RenderSize, mediaSize)))
.ToList();
var annotation = await annotationService.SaveAnnotation(formState.MediaName, annName, timeImg, currentDetections, token: cancellationToken);
return [annotation];
}
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
{
try
{
mainWindow.Dispatcher.Invoke(() =>
{
var namesSet = notification.AnnotationNames.ToHashSet();
var remainAnnotations = formState.AnnotationResults
.Where(x => !namesSet.Contains(x.Name)).ToList();
formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann);
var timedAnnotationsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnotationsToRemove);
if (formState.AnnotationResults.Count == 0)
{
var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name);
if (media != null)
{
media.HasAnnotations = false;
mainWindow.LvFiles.Items.Refresh();
}
}
});
await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
foreach (var name in notification.AnnotationNames)
{
try
{
File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
}
catch (Exception e)
{
logger.LogError(e, e.Message);
}
}
//Only validators can send Delete to the queue
if (!notification.FromQueue && api.CurrentUser.Role.IsValidator())
await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
}
public Task Handle(AnnotationAddedEvent e, CancellationToken ct)
{
mainWindow.Dispatcher.Invoke(() =>
{
var mediaInfo = (MediaFileInfo)mainWindow.LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") == e.Annotation.OriginalMediaName)
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
mainWindow.LvFiles.Items.Refresh();
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
if (media != null)
media.HasAnnotations = true;
mainWindow.LvFiles.Items.Refresh();
mainWindow.StatusHelp.Text = log;
});
return Task.CompletedTask;
}
public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.StatusHelp.Text = e.Text;
mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White;
});
return Task.CompletedTask;
}
public Task Handle(GPSMatcherResultProcessedEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
var ann = mainWindow.MapMatcherComponent.Annotations[e.Index];
AddMarker(e.GeoPoint, e.Image, Brushes.Blue);
if (e.ProcessedGeoPoint != e.GeoPoint)
AddMarker(e.ProcessedGeoPoint, $"{e.Image}: corrected", Brushes.DarkViolet);
ann.Lat = e.GeoPoint.Lat;
ann.Lon = e.GeoPoint.Lon;
});
return Task.CompletedTask;
}
private void AddMarker(GeoPoint point, string text, SolidColorBrush color)
{
var map = mainWindow.MapMatcherComponent;
var pointLatLon = new PointLatLng(point.Lat, point.Lon);
var marker = new GMapMarker(pointLatLon);
marker.Shape = new CircleVisual(marker, size: 14, text: text, background: color);
map.SatelliteMap.Markers.Add(marker);
map.SatelliteMap.Position = pointLatLon;
map.SatelliteMap.ZoomAndCenterMarkers(null);
}
public async Task Handle(AIAvailabilityStatusEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
logger.LogInformation(e.ToString());
mainWindow.AIDetectBtn.IsEnabled = e.Status == AIAvailabilityEnum.Enabled;
mainWindow.StatusHelp.Text = e.ToString();
});
if (e.Status is AIAvailabilityEnum.Enabled or AIAvailabilityEnum.Error)
await inferenceService.CheckAIAvailabilityTokenSource.CancelAsync();
} }
} }
+17 -12
View File
@@ -7,27 +7,32 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<VersionDate>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd"))</VersionDate>
<VersionSeconds>$([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes)))</VersionSeconds>
<AssemblyVersion>$(VersionDate).$(VersionSeconds)</AssemblyVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<InformationalVersion>$(AssemblyVersion)</InformationalVersion>
<Copyright>Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved.</Copyright>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="GMap.NET.WinPresentation" Version="2.1.7" />
<PackageReference Include="libc.translation" Version="7.1.1" /> <PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.9.1" /> <PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RangeTree" Version="3.0.1" /> <PackageReference Include="RangeTree" Version="3.0.1" />
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" /> <PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="System.Data.SQLite.EF6" Version="1.0.119" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" /> <PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+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, int size, string text, Brush background)
{
ShadowEffect = new DropShadowEffect();
Marker = m;
Marker.ZIndex = 100;
SizeChanged += CircleVisual_SizeChanged;
MouseEnter += CircleVisual_MouseEnter;
MouseLeave += CircleVisual_MouseLeave;
Loaded += OnLoaded;
Text = text;
StrokeArrow.EndLineCap = PenLineCap.Triangle;
StrokeArrow.LineJoin = PenLineJoin.Round;
RenderTransform = _scale;
Width = Height = size;
FontSize = Width / 1.55;
Background = background;
Angle = null;
}
void CircleVisual_SizeChanged(object sender, SizeChangedEventArgs e)
{
Marker.Offset = new Point(-e.NewSize.Width / 2, -e.NewSize.Height / 2);
_scale.CenterX = -Marker.Offset.X;
_scale.CenterY = -Marker.Offset.Y;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateVisual(true);
}
readonly ScaleTransform _scale = new ScaleTransform(1, 1);
void CircleVisual_MouseLeave(object sender, MouseEventArgs e)
{
Marker.ZIndex -= 10000;
Cursor = Cursors.Arrow;
Effect = null;
_scale.ScaleY = 1;
_scale.ScaleX = 1;
}
void CircleVisual_MouseEnter(object sender, MouseEventArgs e)
{
Marker.ZIndex += 10000;
Cursor = Cursors.Hand;
Effect = ShadowEffect;
_scale.ScaleY = 1.5;
_scale.ScaleX = 1.5;
}
public DropShadowEffect ShadowEffect;
static readonly Typeface Font = new Typeface(new FontFamily("Arial"),
FontStyles.Normal,
FontWeights.Bold,
FontStretches.Normal);
FormattedText _fText = null!;
private Brush _background = Brushes.Blue;
public Brush Background
{
get
{
return _background;
}
set
{
if (_background != value)
{
_background = value;
IsChanged = true;
}
}
}
private Brush _foreground = Brushes.White;
public Brush Foreground
{
get
{
return _foreground;
}
set
{
if (_foreground != value)
{
_foreground = value;
IsChanged = true;
ForceUpdateText();
}
}
}
private Pen _stroke = new Pen(Brushes.Blue, 2.0);
public Pen Stroke
{
get
{
return _stroke;
}
set
{
if (_stroke != value)
{
_stroke = value;
IsChanged = true;
}
}
}
private Pen _strokeArrow = new Pen(Brushes.Blue, 2.0);
public Pen StrokeArrow
{
get
{
return _strokeArrow;
}
set
{
if (_strokeArrow != value)
{
_strokeArrow = value;
IsChanged = true;
}
}
}
public double FontSize = 16;
private double? _angle = 0;
public double? Angle
{
get => _angle;
set
{
if (!_angle.HasValue || !value.HasValue ||
Angle.HasValue && Math.Abs(_angle.Value - value.Value) > 11)
{
_angle = value;
IsChanged = true;
}
}
}
public bool IsChanged = true;
void ForceUpdateText()
{
_fText = new FormattedText(_text,
CultureInfo.InvariantCulture,
FlowDirection.LeftToRight,
Font,
FontSize,
Foreground, 1.0);
IsChanged = true;
}
string _text = null!;
public string Text
{
get
{
return _text;
}
set
{
if (_text != value)
{
_text = value;
ForceUpdateText();
}
}
}
Visual _child = null!;
public virtual Visual? Child
{
get => _child;
set
{
if (_child == value)
return;
if (_child != null)
{
RemoveLogicalChild(_child);
RemoveVisualChild(_child);
}
if (value != null)
{
AddVisualChild(value);
AddLogicalChild(value);
}
// cache the new child
_child = value!;
InvalidateVisual();
}
}
public bool UpdateVisual(bool forceUpdate)
{
if (forceUpdate || IsChanged)
{
Child = Create();
IsChanged = false;
return true;
}
return false;
}
int _countCreate;
private DrawingVisual Create()
{
_countCreate++;
var square = new DrawingVisualFx();
using var dc = square.RenderOpen();
dc.DrawEllipse(null,
Stroke,
new Point(Width / 2, Height / 2),
Width / 2 + Stroke.Thickness / 2,
Height / 2 + Stroke.Thickness / 2);
if (Angle.HasValue)
{
dc.PushTransform(new RotateTransform(Angle.Value, Width / 2, Height / 2));
{
var polySeg = new PolyLineSegment(new[]
{
new Point(Width * 0.2, Height * 0.3), new Point(Width * 0.8, Height * 0.3)
},
true);
var pathFig = new PathFigure(new Point(Width * 0.5, -Height * 0.22),
new PathSegment[] {polySeg},
true);
var pathGeo = new PathGeometry(new[] {pathFig});
dc.DrawGeometry(Brushes.AliceBlue, StrokeArrow, pathGeo);
}
dc.Pop();
}
dc.DrawEllipse(Background, null, new Point(Width / 2, Height / 2), Width / 2, Height / 2);
dc.DrawText(_fText, new Point(Width / 2 - _fText.Width / 2, Height / 2 - _fText.Height / 2));
return square;
}
#region Necessary Overrides -- Needed by WPF to maintain bookkeeping of our hosted visuals
protected override int VisualChildrenCount
{
get
{
return Child == null ? 0 : 1;
}
}
protected override Visual? GetVisualChild(int index)
{
return Child;
}
#endregion
}
public class DrawingVisualFx : DrawingVisual
{
public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect",
typeof(Effect),
typeof(DrawingVisualFx),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.AffectsRender,
OnEffectChanged));
public new Effect Effect
{
get
{
return (Effect)GetValue(EffectProperty);
}
set
{
SetValue(EffectProperty, value);
}
}
private static void OnEffectChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var drawingVisualFx = o as DrawingVisualFx;
if (drawingVisualFx != null)
{
drawingVisualFx.SetMyProtectedVisualEffect((Effect)e.NewValue);
}
}
private void SetMyProtectedVisualEffect(Effect effect)
{
VisualEffect = effect;
}
}
}
+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"/>
<Border Grid.Column="2" ClipToBounds="True">
<controls:CanvasEditor
x:Name="GpsImageEditor"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
</Border>
<GridSplitter
Background="DarkGray"
ResizeDirection="Columns"
Grid.Column="3"
ResizeBehavior="PreviousAndNext"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<windowsPresentation:GMapControl
Grid.Column="4"
x:Name="SatelliteMap"
Zoom="20" MaxZoom="24" MinZoom="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
MinWidth="400" />
</Grid>
</UserControl>
@@ -0,0 +1,118 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using GMap.NET;
using GMap.NET.MapProviders;
using Microsoft.WindowsAPICodePack.Dialogs;
namespace Azaion.Annotator.Controls;
public partial class MapMatcher : UserControl
{
private AppConfig _appConfig = null!;
List<MediaFileInfo> _allMediaFiles = new();
public Dictionary<int, Annotation> Annotations = new();
private string _currentDir = null!;
private IGpsMatcherService _gpsMatcherService = null!;
public MapMatcher()
{
InitializeComponent();
}
public void Init(AppConfig appConfig, IGpsMatcherService gpsMatcherService)
{
_appConfig = appConfig;
_gpsMatcherService = gpsMatcherService;
GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey;
SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap;
SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501);
SatelliteMap.MultiTouchEnabled = true;
GpsFiles.MouseDoubleClick += async (sender, args) => { await OpenGpsLocation(GpsFiles.SelectedIndex); };
}
private async Task OpenGpsLocation(int gpsFilesIndex)
{
//var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo;
var ann = Annotations.GetValueOrDefault(gpsFilesIndex);
if (ann == null)
return;
GpsImageEditor.Background = new ImageBrush
{
ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage()
};
if (ann.Lat != 0 && ann.Lon != 0)
SatelliteMap.Position = new PointLatLng(ann.Lat, ann.Lon);
}
private void GpsFilesContextOpening(object sender, ContextMenuEventArgs e)
{
var listItem = sender as ListViewItem;
GpsFilesContextMenu.DataContext = listItem!.DataContext;
}
private void OpenContainingFolder(object sender, RoutedEventArgs e)
{
var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo;
if (mediaFileInfo == null)
return;
Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\"");
}
private async void OpenGpsTilesFolderClick(object sender, RoutedEventArgs e)
{
var dlg = new CommonOpenFileDialog
{
Title = "Open Video folder",
IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)
};
var dialogResult = dlg.ShowDialog();
if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName))
return;
TbGpsMapFolder.Text = dlg.FileName;
_currentDir = dlg.FileName;
var dir = new DirectoryInfo(dlg.FileName);
var mediaFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray())
.Select(x => new MediaFileInfo
{
Name = x.Name,
Path = x.FullName,
MediaType = MediaTypes.Image
}).ToList();
_allMediaFiles = mediaFiles;
GpsFiles.ItemsSource = new ObservableCollection<MediaFileInfo>(_allMediaFiles);
Annotations = mediaFiles.Select((x, i) => (i, new Annotation
{
Name = x.Name,
OriginalMediaName = x.Name
})).ToDictionary(x => x.i, x => x.Item2);
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLatLon);
}
private async void TestGps(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(TbGpsMapFolder.Text))
return;
var initialLatLon = new GeoPoint(double.Parse(TbLat.Text), double.Parse(TbLon.Text));
await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLatLon);
}
}
+1 -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,8 +0,0 @@
using System.Drawing;
namespace Azaion.Annotator.Extensions;
public static class RectangleFExtensions
{
public static double Area(this RectangleF rectangle) => rectangle.Width * rectangle.Height;
}
@@ -1,14 +0,0 @@
using System.ComponentModel;
namespace Azaion.Annotator;
public static class SynchronizeInvokeExtensions
{
public static void InvokeEx<T>(this T t, Action<T> action) where T : ISynchronizeInvoke
{
if (t.InvokeRequired)
t.Invoke(action, [t]);
else
action(t);
}
}
@@ -1,130 +0,0 @@
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Azaion.Common.DTO.Config;
using LibVLCSharp.Shared;
using Microsoft.Extensions.Options;
using SkiaSharp;
namespace Azaion.Annotator.Extensions;
public class VLCFrameExtractor(LibVLC libVLC, IOptions<AIRecognitionConfig> config)
{
private const uint RGBA_BYTES = 4;
private const int PLAYBACK_RATE = 4;
private uint _pitch; // Number of bytes per "line", aligned to x32.
private uint _lines; // Number of lines in the buffer, aligned to x32.
private uint _width; // Thumbnail width
private uint _height; // Thumbnail height
private MediaPlayer _mediaPlayer = null!;
private TimeSpan _lastFrameTimestamp;
private long _lastFrame;
private static uint Align32(uint size)
{
if (size % 32 == 0)
return size;
return (size / 32 + 1) * 32;// Align on the next multiple of 32
}
private static SKBitmap? _currentBitmap;
private static readonly ConcurrentQueue<FrameInfo> FramesQueue = new();
private static long _frameCounter;
public async IAsyncEnumerable<(TimeSpan Time, Stream Stream)> ExtractFrames(string mediaPath,
[EnumeratorCancellation] CancellationToken manualCancellationToken = default)
{
var videoFinishedCancellationSource = new CancellationTokenSource();
_mediaPlayer = new MediaPlayer(libVLC);
_mediaPlayer.Stopped += (s, e) => videoFinishedCancellationSource.CancelAfter(1);
using var media = new Media(libVLC, mediaPath);
await media.Parse(cancellationToken: videoFinishedCancellationSource.Token);
var videoTrack = media.Tracks.FirstOrDefault(x => x.Data.Video.Width != 0);
_width = videoTrack.Data.Video.Width;
_height = videoTrack.Data.Video.Height;
_pitch = Align32(_width * RGBA_BYTES);
_lines = Align32(_height);
_mediaPlayer.SetRate(PLAYBACK_RATE);
media.AddOption(":no-audio");
_mediaPlayer.SetVideoFormat("RV32", _width, _height, _pitch);
_mediaPlayer.SetVideoCallbacks(Lock, null, Display);
_mediaPlayer.Play(media);
_frameCounter = 0;
var surface = SKSurface.Create(new SKImageInfo((int) _width, (int) _height));
var videoFinishedCT = videoFinishedCancellationSource.Token;
while ( !(FramesQueue.IsEmpty && videoFinishedCT.IsCancellationRequested || manualCancellationToken.IsCancellationRequested))
{
if (FramesQueue.TryDequeue(out var frameInfo))
{
if (frameInfo.Bitmap == null)
continue;
surface.Canvas.DrawBitmap(frameInfo.Bitmap, 0, 0); // Effectively crops the original bitmap to get only the visible area
using var outputImage = surface.Snapshot();
using var data = outputImage.Encode(SKEncodedImageFormat.Jpeg, 85);
using var ms = new MemoryStream();
data.SaveTo(ms);
yield return (frameInfo.Time, ms);
frameInfo.Bitmap?.Dispose();
}
else
{
await Task.Delay(TimeSpan.FromSeconds(1), videoFinishedCT);
}
}
FramesQueue.Clear(); //clear queue in case of manual stop
_mediaPlayer.Stop();
_mediaPlayer.Dispose();
}
private IntPtr Lock(IntPtr opaque, IntPtr planes)
{
_currentBitmap = new SKBitmap(new SKImageInfo((int)(_pitch / RGBA_BYTES), (int)_lines, SKColorType.Bgra8888));
Marshal.WriteIntPtr(planes, _currentBitmap.GetPixels());
return IntPtr.Zero;
}
private void Display(IntPtr opaque, IntPtr picture)
{
var playerTime = TimeSpan.FromMilliseconds(_mediaPlayer.Time);
if (_lastFrameTimestamp != playerTime)
{
_lastFrame = _frameCounter;
_lastFrameTimestamp = playerTime;
}
if (_frameCounter > 20 && _frameCounter % config.Value.FramePeriodRecognition == 0)
{
var msToAdd = (_frameCounter - _lastFrame) * (_lastFrame == 0 ? 0 : _lastFrameTimestamp.TotalMilliseconds / _lastFrame);
var time = _lastFrameTimestamp.Add(TimeSpan.FromMilliseconds(msToAdd));
FramesQueue.Enqueue(new FrameInfo(time, _currentBitmap));
}
else
{
_currentBitmap?.Dispose();
}
_currentBitmap = null;
_frameCounter++;
}
}
public class FrameInfo(TimeSpan time, SKBitmap? bitmap)
{
public TimeSpan Time { get; set; } = time;
public SKBitmap? Bitmap { get; set; } = bitmap;
}
-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();
}
+16 -9
View File
@@ -4,22 +4,29 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<LangVersion>12</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="LazyCache" Version="2.4.0" />
<PackageReference Include="linq2db.SQLite" Version="5.4.1" /> <PackageReference Include="linq2db.SQLite" Version="5.4.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.5.0" />
<PackageReference Include="MessagePack" Version="3.1.0" /> <PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
<PackageReference Include="NetMQ" Version="4.0.1.16" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" /> <PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
<PackageReference Include="System.Drawing.Common" Version="4.7.3" /> <PackageReference Include="Serilog" Version="4.3.0" />
</ItemGroup> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="Stub.System.Data.SQLite.Core.NetStandard" Version="1.0.119" />
<ItemGroup> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" /> <PackageReference Include="System.Drawing.Common" Version="5.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+226 -56
View File
@@ -1,97 +1,186 @@
using System.Windows; using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Newtonsoft.Json;
using Serilog;
using System.Windows;
namespace Azaion.Common; namespace Azaion.Common;
public class Constants public static class Constants
{ {
public const string SECURE_RESOURCE_CACHE = "SecureResourceCache"; public const string CONFIG_PATH = "config.json";
public const string DEFAULT_API_URL = "https://api.azaion.com";
public const string AZAION_SUITE_EXE = "Azaion.Suite.exe";
public const int AI_TILE_SIZE_DEFAULT = 1280;
#region ExternalClientsConfig
private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_LOADER_PORT = 5025;
private static readonly LoaderClientConfig DefaultLoaderClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST,
ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT,
ApiUrl = DEFAULT_API_URL
};
public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe";
public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe";
public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied";
public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe");
public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_INFERENCE_PORT = 5227;
private static readonly InferenceClientConfig DefaultInferenceClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST,
ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT,
ApiUrl = DEFAULT_API_URL
};
private const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1";
private const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5255;
private const int DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT = 5256;
private static readonly GpsDeniedClientConfig DefaultGpsDeniedClientConfig = new()
{
ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST,
ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT,
ZeroMqReceiverPort = DEFAULT_ZMQ_GPS_DENIED_PUBLISH_PORT
};
#endregion ExternalClientsConfig
public const string CURRENT_USER_CACHE_KEY = "CurrentUser";
public const string JPG_EXT = ".jpg";
public const string TXT_EXT = ".txt";
#region DirectoriesConfig #region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video"; public const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels"; private const string DEFAULT_LABELS_DIR = "labels";
public const string DEFAULT_IMAGES_DIR = "images"; private const string DEFAULT_IMAGES_DIR = "images";
public const string DEFAULT_RESULTS_DIR = "results"; private const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; private const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
private const string DEFAULT_GPS_SAT_DIRECTORY = "satellitesDir";
private const string DEFAULT_GPS_ROUTE_DIRECTORY = "routeDir";
#endregion #endregion
#region AnnotatorConfig #region AnnotatorConfig
public static readonly List<DetectionClass> DefaultAnnotationClasses = public static readonly List<DetectionClass> DefaultAnnotationClasses =
[ [
new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor(), MaxSizeM = 7 },
new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor(), MaxSizeM = 8 },
new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" }, new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor(), MaxSizeM = 7 },
new() { Id = 3, Name = "Артилерія", ShortName = "Арта" }, new() { Id = 3, Name = "Artillery", ShortName = "Арта", Color = "#FFFF00".ToColor(), MaxSizeM = 14 },
new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" }, new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor(), MaxSizeM = 9 },
new() { Id = 5, Name = "Окопи", ShortName = "Окопи" }, new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor(), MaxSizeM = 10 },
new() { Id = 6, Name = "Військовий", ShortName = "Військов" }, new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor(), MaxSizeM = 2 },
new() { Id = 7, Name = "Накати", ShortName = "Накати" }, new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor(), MaxSizeM = 5 },
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, new() { Id = 8, Name = "AdditionArmoredTank",ShortName = "Танк.захист", Color = "#008000".ToColor(), MaxSizeM = 7 },
new() { Id = 9, Name = "Дим", ShortName = "Дим" }, new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor(), MaxSizeM = 8 },
new() { Id = 10, Name = "Літак", ShortName = "Літак" }, new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor(), MaxSizeM = 12 },
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor(), MaxSizeM = 3 },
new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor(), MaxSizeM = 14 },
new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor(), MaxSizeM = 8 },
new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor(), MaxSizeM = 15 },
new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor(), MaxSizeM = 20 },
new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor(), MaxSizeM = 10 },
new() { Id = 17, Name = "Ammo", ShortName = "БК", Color = "#33658a".ToColor(), MaxSizeM = 2 },
new() { Id = 18, Name = "Protect.Struct", ShortName = "Зуби.драк", Color = "#969647".ToColor(), MaxSizeM = 2 }
]; ];
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi", "ts"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
public static int DEFAULT_LEFT_PANEL_WIDTH = 250; private static readonly AnnotationConfig DefaultAnnotationConfig = new()
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250; {
DetectionClasses = DefaultAnnotationClasses,
VideoFormats = DefaultVideoFormats,
ImageFormats = DefaultImageFormats,
AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE
};
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db"; #region UIConfig
public const int DEFAULT_LEFT_PANEL_WIDTH = 200;
public const int DEFAULT_RIGHT_PANEL_WIDTH = 200;
#endregion UIConfig
#region CameraConfig
public const int DEFAULT_ALTITUDE = 400;
public const decimal DEFAULT_CAMERA_FOCAL_LENGTH = 24m;
public const decimal DEFAULT_CAMERA_SENSOR_WIDTH = 23.5m;
public static readonly CameraConfig DefaultCameraConfig = new()
{
Altitude = DEFAULT_ALTITUDE,
CameraFocalLength = DEFAULT_CAMERA_FOCAL_LENGTH,
CameraSensorWidth = DEFAULT_CAMERA_SENSOR_WIDTH
};
#endregion CameraConfig
private const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
# endregion AnnotatorConfig # endregion AnnotatorConfig
# region AIRecognitionConfig # region AIRecognitionConfig
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; private static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new()
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; {
public const double TRACKING_PROBABILITY_INCREASE = 15; FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS,
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE,
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD,
BigImageTileOverlapPercent = DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT,
FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION
};
private const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
private const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
private const double TRACKING_PROBABILITY_INCREASE = 15;
private const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
private const int DEFAULT_BIG_IMAGE_TILE_OVERLAP_PERCENT = 20;
private const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
# endregion AIRecognitionConfig # endregion AIRecognitionConfig
# region GpsDeniedConfig
private static readonly GpsDeniedConfig DefaultGpsDeniedConfig = new()
{
MinKeyPoints = 11
};
# endregion
#region Thumbnails #region Thumbnails
public static readonly Size DefaultThumbnailSize = new(240, 135); private static readonly Size DefaultThumbnailSize = new(240, 135);
public const int DEFAULT_THUMBNAIL_BORDER = 10; private static readonly ThumbnailConfig DefaultThumbnailConfig = new()
{
Size = DefaultThumbnailSize,
Border = DEFAULT_THUMBNAIL_BORDER
};
private const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb"; public const string THUMBNAIL_PREFIX = "_thumb";
public const string RESULT_PREFIX = "_result";
#endregion #endregion
public static TimeSpan? GetTime(string imagePath)
{
var timeStr = imagePath.Split("_").LastOrDefault();
if (string.IsNullOrEmpty(timeStr) || timeStr.Length < 6)
return null;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!int.TryParse(timeStr[0..1], out var hours))
return null;
if (!int.TryParse(timeStr[1..3], out var minutes))
return null;
if (!int.TryParse(timeStr[3..5], out var seconds))
return null;
if (!int.TryParse(timeStr[5..6], out var milliseconds))
return null;
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
}
#region Queue
public const string MQ_DIRECT_TYPE = "direct";
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
#endregion
#region Database #region Database
@@ -99,7 +188,88 @@ public class Constants
public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue"; public const string ANNOTATIONS_QUEUE_TABLENAME = "annotations_queue";
public const string ADMIN_EMAIL = "admin@azaion.com"; public const string ADMIN_EMAIL = "admin@azaion.com";
public const string DETECTIONS_TABLENAME = "detections"; public const string DETECTIONS_TABLENAME = "detections";
public const string MEDIAFILE_TABLENAME = "mediafiles";
#endregion #endregion
#region Mode Captions
public const string REGULAR_MODE_CAPTION = "Норма";
public const string WINTER_MODE_CAPTION = "Зима";
public const string NIGHT_MODE_CAPTION = "Ніч";
#endregion
public const string SPLIT_SUFFIX = "!split!";
private static readonly InitConfig DefaultInitConfig = new()
{
LoaderClientConfig = DefaultLoaderClientConfig,
InferenceClientConfig = DefaultInferenceClientConfig,
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
DirectoriesConfig = new DirectoriesConfig
{
ApiResourcesDirectory = ""
},
CameraConfig = DefaultCameraConfig
};
public static readonly AppConfig FailsafeAppConfig = new()
{
AnnotationConfig = DefaultAnnotationConfig,
UIConfig = new UIConfig
{
LeftPanelWidth = DEFAULT_LEFT_PANEL_WIDTH,
RightPanelWidth = DEFAULT_RIGHT_PANEL_WIDTH,
GenerateAnnotatedImage = false
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = DEFAULT_VIDEO_DIR,
ImagesDirectory = DEFAULT_IMAGES_DIR,
LabelsDirectory = DEFAULT_LABELS_DIR,
ResultsDirectory = DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = DEFAULT_THUMBNAILS_DIR,
GpsSatDirectory = DEFAULT_GPS_SAT_DIRECTORY,
GpsRouteDirectory = DEFAULT_GPS_ROUTE_DIRECTORY
},
ThumbnailConfig = DefaultThumbnailConfig,
AIRecognitionConfig = DefaultAIRecognitionConfig,
GpsDeniedConfig = DefaultGpsDeniedConfig,
LoaderClientConfig = DefaultLoaderClientConfig,
InferenceClientConfig = DefaultInferenceClientConfig,
GpsDeniedClientConfig = DefaultGpsDeniedClientConfig,
CameraConfig = DefaultCameraConfig
};
public static InitConfig ReadInitConfig(ILogger logger)
{
try
{
if (!File.Exists(CONFIG_PATH))
throw new FileNotFoundException(CONFIG_PATH);
var configStr = File.ReadAllText(CONFIG_PATH);
var config = JsonConvert.DeserializeObject<InitConfig>(configStr);
return config ?? DefaultInitConfig;
}
catch (Exception e)
{
logger.Error(e, e.Message);
return DefaultInitConfig;
}
}
public static Version GetLocalVersion()
{
var localFileInfo = FileVersionInfo.GetVersionInfo(AZAION_SUITE_EXE);
if (string.IsNullOrWhiteSpace(localFileInfo.ProductVersion))
throw new Exception($"Can't find {AZAION_SUITE_EXE} and its version");
return new Version(localFileInfo.FileVersion!);
}
} }
@@ -0,0 +1,71 @@
<UserControl x:Class="Azaion.Common.Controls.CameraConfigControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:cfg="clr-namespace:Azaion.Common.DTO.Config"
xmlns:controls="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="120" d:DesignWidth="360">
<Grid Margin="4" Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="65"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="70"/>
</Grid.ColumnDefinitions>
<!-- Altitude -->
<TextBlock Grid.Row="0" Grid.Column="0"
Foreground="LightGray"
VerticalAlignment="Center" Margin="0,0,8,0" Text="Altitude, m:"/>
<Slider x:Name="AltitudeSlider" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Minimum="0" Maximum="10000" TickFrequency="100"
IsSnapToTickEnabled="False"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<controls:NumericUpDown x:Name="AltitudeNud"
Grid.Row="0" Grid.Column="3"
VerticalAlignment="Center"
MinValue="50"
MaxValue="5000"
Value="{Binding Camera.Altitude, RelativeSource={RelativeSource AncestorType=UserControl},
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Step="10">
</controls:NumericUpDown>
<!-- Focal length -->
<TextBlock
Foreground="LightGray"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Margin="0,8,8,0" Text="Focal length, mm:"/>
<controls:NumericUpDown x:Name="FocalNud"
Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
MinValue="0.1"
MaxValue="100"
Step="0.05"
VerticalAlignment="Center"
Value="{Binding Camera.CameraFocalLength, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
<!-- Sensor width -->
<TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center"
Foreground="LightGray"
Margin="0,8,8,0" Text="Sensor width, mm:"/>
<controls:NumericUpDown x:Name="SensorNud"
Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Step="0.05"
VerticalAlignment="Center"
MinValue="0.1"
MaxValue="100"
Value="{Binding Camera.CameraSensorWidth, RelativeSource={RelativeSource AncestorType=UserControl}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</controls:NumericUpDown>
</Grid>
</UserControl>
@@ -0,0 +1,56 @@
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using Azaion.Common.DTO.Config;
namespace Azaion.Common.Controls;
public partial class CameraConfigControl
{
public static readonly DependencyProperty CameraProperty = DependencyProperty.Register(
nameof(Camera), typeof(CameraConfig), typeof(CameraConfigControl),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public CameraConfig Camera
{
get => (CameraConfig)GetValue(CameraProperty) ?? new CameraConfig();
set => SetValue(CameraProperty, value);
}
// Fires whenever any camera parameter value changes in UI
public event EventHandler? CameraChanged;
public CameraConfigControl()
{
InitializeComponent();
DataContext = this;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
// Hook up change notifications
if (AltitudeSlider != null)
AltitudeSlider.ValueChanged += (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty);
SubscribeNud(AltitudeNud);
SubscribeNud(FocalNud);
SubscribeNud(SensorNud);
}
private void SubscribeNud(UserControl? nud)
{
if (nud is NumericUpDown num)
{
var dpd = DependencyPropertyDescriptor.FromProperty(NumericUpDown.ValueProperty, typeof(NumericUpDown));
dpd?.AddValueChanged(num, (_, __) => CameraChanged?.Invoke(this, EventArgs.Empty));
}
}
// Initializes the control with the provided CameraConfig instance and wires two-way binding via dependency property
public void Init(CameraConfig cameraConfig)
{
Camera = cameraConfig;
}
}
+263 -76
View File
@@ -1,13 +1,19 @@
using System.Windows; using System.Drawing;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Annotator.DTO; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.Extensions;
using MediatR; using MediatR;
using Color = System.Windows.Media.Color; using Color = System.Windows.Media.Color;
using Image = System.Windows.Controls.Image;
using Point = System.Windows.Point;
using Rectangle = System.Windows.Shapes.Rectangle; using Rectangle = System.Windows.Shapes.Rectangle;
using Size = System.Windows.Size;
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
@@ -26,21 +32,26 @@ public class CanvasEditor : Canvas
private Rectangle _curRec = new(); private Rectangle _curRec = new();
private DetectionControl _curAnn = null!; private DetectionControl _curAnn = null!;
private const int MIN_SIZE = 20; private readonly MatrixTransform _matrixTransform = new();
private Point _panStartPoint;
private bool _isZoomedIn;
private const int MIN_SIZE = 12;
private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400);
public IMediator Mediator { get; set; } = null!; public Image BackgroundImage { get; set; } = new() { Stretch = Stretch.Uniform };
private RectangleF? _clampedRect;
public static readonly DependencyProperty GetTimeFuncProp = public static readonly DependencyProperty GetTimeFuncProp =
DependencyProperty.Register( DependencyProperty.Register(
nameof(GetTimeFunc), nameof(GetTimeFunc),
typeof(Func<TimeSpan?>), typeof(Func<TimeSpan>),
typeof(CanvasEditor), typeof(CanvasEditor),
new PropertyMetadata(null)); new PropertyMetadata(null));
public Func<TimeSpan?> GetTimeFunc public Func<TimeSpan> GetTimeFunc
{ {
get => (Func<TimeSpan?>)GetValue(GetTimeFuncProp); get => (Func<TimeSpan>)GetValue(GetTimeFuncProp);
set => SetValue(GetTimeFuncProp, value); set => SetValue(GetTimeFuncProp, value);
} }
@@ -54,7 +65,7 @@ public class CanvasEditor : Canvas
_verticalLine.Fill = value.ColorBrush; _verticalLine.Fill = value.ColorBrush;
_horizontalLine.Stroke = value.ColorBrush; _horizontalLine.Stroke = value.ColorBrush;
_horizontalLine.Fill = value.ColorBrush; _horizontalLine.Fill = value.ColorBrush;
_classNameHint.Text = value.Name; _classNameHint.Text = value.ShortName;
_newAnnotationRect.Stroke = value.ColorBrush; _newAnnotationRect.Stroke = value.ColorBrush;
_newAnnotationRect.Fill = value.ColorBrush; _newAnnotationRect.Fill = value.ColorBrush;
@@ -84,7 +95,7 @@ public class CanvasEditor : Canvas
}; };
_classNameHint = new TextBlock _classNameHint = new TextBlock
{ {
Text = CurrentAnnClass?.Name ?? "asd", Text = CurrentAnnClass?.ShortName ?? "",
Foreground = new SolidColorBrush(Colors.Black), Foreground = new SolidColorBrush(Colors.Black),
Cursor = Cursors.Arrow, Cursor = Cursors.Arrow,
FontSize = 16, FontSize = 16,
@@ -99,22 +110,61 @@ public class CanvasEditor : Canvas
Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)), Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)),
}; };
KeyDown += (_, args) =>
{
Console.WriteLine($"pressed {args.Key}");
};
MouseDown += CanvasMouseDown; MouseDown += CanvasMouseDown;
MouseMove += CanvasMouseMove; MouseMove += CanvasMouseMove;
MouseUp += CanvasMouseUp; MouseUp += CanvasMouseUp;
SizeChanged += CanvasResized; SizeChanged += CanvasResized;
Cursor = Cursors.Cross; Cursor = Cursors.Cross;
Children.Insert(0, BackgroundImage);
Children.Add(_newAnnotationRect); Children.Add(_newAnnotationRect);
Children.Add(_horizontalLine); Children.Add(_horizontalLine);
Children.Add(_verticalLine); Children.Add(_verticalLine);
Children.Add(_classNameHint); Children.Add(_classNameHint);
Loaded += Init; Loaded += Init;
RenderTransform = _matrixTransform;
MouseWheel += CanvasWheel;
}
public void SetBackground(ImageSource? source)
{
SetZoom();
BackgroundImage.Source = source;
UpdateClampedRect();
}
private void SetZoom(Matrix? matrix = null)
{
if (matrix == null)
{
_matrixTransform.Matrix = Matrix.Identity;
_isZoomedIn = false;
}
else
{
_matrixTransform.Matrix = matrix.Value;
_isZoomedIn = true;
}
// foreach (var detection in CurrentDetections)
// detection.UpdateAdornerScale(scale: _matrixTransform.Matrix.M11);
}
private void CanvasWheel(object sender, MouseWheelEventArgs e)
{
if (Keyboard.Modifiers != ModifierKeys.Control)
return;
var mousePos = e.GetPosition(this);
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
var matrix = _matrixTransform.Matrix;
if (scale < 1 && matrix.M11 * scale < 1.0)
SetZoom();
else
{
matrix.ScaleAt(scale, scale, mousePos.X, mousePos.Y);
SetZoom(matrix);
}
} }
private void Init(object sender, RoutedEventArgs e) private void Init(object sender, RoutedEventArgs e)
@@ -128,42 +178,165 @@ public class CanvasEditor : Canvas
private void CanvasMouseDown(object sender, MouseButtonEventArgs e) private void CanvasMouseDown(object sender, MouseButtonEventArgs e)
{ {
ClearSelections(); ClearSelections();
if (e.LeftButton != MouseButtonState.Pressed)
return;
if (Keyboard.Modifiers == ModifierKeys.Control && _isZoomedIn)
{
_panStartPoint = e.GetPosition(this);
SelectionState = SelectionState.PanZoomMoving;
}
else
NewAnnotationStart(sender, e); NewAnnotationStart(sender, e);
(sender as UIElement)?.CaptureMouse();
} }
private void CanvasMouseMove(object sender, MouseEventArgs e) private void CanvasMouseMove(object sender, MouseEventArgs e)
{ {
var pos = e.GetPosition(this); var pos = GetClampedPosition(e);
_horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y; _horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y;
_verticalLine.X1 = _verticalLine.X2 = pos.X; _verticalLine.X1 = _verticalLine.X2 = pos.X;
SetLeft(_classNameHint, pos.X + 10); SetLeft(_classNameHint, pos.X + 10);
SetTop(_classNameHint, pos.Y - 30); SetTop(_classNameHint, pos.Y - 30);
if (e.LeftButton != MouseButtonState.Pressed) switch (SelectionState)
return; {
if (SelectionState == SelectionState.NewAnnCreating) case SelectionState.NewAnnCreating:
NewAnnotationCreatingMove(sender, e); NewAnnotationCreatingMove(pos);
break;
case SelectionState.AnnResizing:
AnnotationResizeMove(pos);
break;
case SelectionState.AnnMoving:
AnnotationPositionMove(pos);
e.Handled = true;
break;
case SelectionState.PanZoomMoving:
PanZoomMove(pos);
break;
}
}
if (SelectionState == SelectionState.AnnResizing) private Point GetClampedPosition(MouseEventArgs e)
AnnotationResizeMove(sender, e); {
var pos = e.GetPosition(this);
return !_clampedRect.HasValue
? pos
: new Point
(
Math.Clamp(pos.X, _clampedRect.Value.X, _clampedRect.Value.Right),
Math.Clamp(pos.Y, _clampedRect.Value.Y, _clampedRect.Value.Bottom)
);
}
if (SelectionState == SelectionState.AnnMoving) private void PanZoomMove(Point point)
AnnotationPositionMove(sender, e); {
var delta = point - _panStartPoint;
var matrix = _matrixTransform.Matrix;
matrix.Translate(delta.X, delta.Y);
_matrixTransform.Matrix = matrix;
} }
private void CanvasMouseUp(object sender, MouseButtonEventArgs e) private void CanvasMouseUp(object sender, MouseButtonEventArgs e)
{ {
(sender as UIElement)?.ReleaseMouseCapture();
if (SelectionState == SelectionState.NewAnnCreating) if (SelectionState == SelectionState.NewAnnCreating)
CreateAnnotation(e.GetPosition(this)); {
var endPos = GetClampedPosition(e);
_newAnnotationRect.Width = 0;
_newAnnotationRect.Height = 0;
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y);
if (width >= MIN_SIZE && height >= MIN_SIZE)
{
var time = GetTimeFunc();
var control = CreateDetectionControl(CurrentAnnClass, time, new CanvasLabel
{
Width = width,
Height = height,
Left = Math.Min(endPos.X, _newAnnotationStartPos.X),
Top = Math.Min(endPos.Y, _newAnnotationStartPos.Y),
Confidence = 1
});
control.UpdateLayout();
CheckLabelBoundaries(control);
}
}
else if (SelectionState != SelectionState.PanZoomMoving)
CheckLabelBoundaries(_curAnn);
SelectionState = SelectionState.None; SelectionState = SelectionState.None;
e.Handled = true; e.Handled = true;
} }
private void CheckLabelBoundaries(DetectionControl detectionControl)
{
var lb = detectionControl.DetectionLabelContainer;
var origin = lb.TranslatePoint(new Point(0, 0), this);
lb.Children[0].Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var size = lb.Children[0].DesiredSize;
var controlLabel = new RectangleF((float)origin.X, (float)origin.Y, (float)size.Width, (float)size.Height);
foreach (var c in CurrentDetections)
{
if (c == detectionControl)
continue;
var detRect = new RectangleF((float)GetLeft(c), (float)GetTop(c), (float)c.Width, (float)c.Height);
detRect.Intersect(controlLabel);
// var intersect = detections[i].ToRectangle();
// intersect.Intersect(detections[j].ToRectangle());
// detectionControl.
// var otherControls = allControls.Where(c => c != control);
// control.UpdateLabelPosition(otherControls);
}
}
private void CanvasResized(object sender, SizeChangedEventArgs e) private void CanvasResized(object sender, SizeChangedEventArgs e)
{ {
_horizontalLine.X2 = e.NewSize.Width; _horizontalLine.X2 = e.NewSize.Width;
_verticalLine.Y2 = e.NewSize.Height; _verticalLine.Y2 = e.NewSize.Height;
BackgroundImage.Width = e.NewSize.Width;
BackgroundImage.Height = e.NewSize.Height;
UpdateClampedRect();
}
private void UpdateClampedRect()
{
if (BackgroundImage.Source is not BitmapSource imageSource)
{
_clampedRect = null;
return;
}
var imgWidth = imageSource.PixelWidth;
var imgHeight = imageSource.PixelHeight;
var canvasWidth = ActualWidth;
var canvasHeight = ActualHeight;
var imgRatio = imgWidth / (double)imgHeight;
var canvasRatio = canvasWidth / canvasHeight;
double renderedWidth;
double renderedHeight;
if (imgRatio > canvasRatio)
{
renderedWidth = canvasWidth;
renderedHeight = canvasWidth / imgRatio;
}
else
{
renderedHeight = canvasHeight;
renderedWidth = canvasHeight * imgRatio;
}
var xOffset = (canvasWidth - renderedWidth) / 2;
var yOffset = (canvasHeight - renderedHeight) / 2;
_clampedRect = new RectangleF((float)xOffset, (float)yOffset, (float)renderedWidth, (float)renderedHeight);
} }
#region Annotation Resizing & Moving #region Annotation Resizing & Moving
@@ -174,20 +347,19 @@ public class CanvasEditor : Canvas
_lastPos = e.GetPosition(this); _lastPos = e.GetPosition(this);
_curRec = (Rectangle)sender; _curRec = (Rectangle)sender;
_curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent; _curAnn = (DetectionControl)((Grid)_curRec.Parent).Parent;
(sender as UIElement)?.CaptureMouse();
e.Handled = true; e.Handled = true;
} }
private void AnnotationResizeMove(object sender, MouseEventArgs e) private void AnnotationResizeMove(Point point)
{ {
if (SelectionState != SelectionState.AnnResizing) if (SelectionState != SelectionState.AnnResizing)
return; return;
var currentPos = e.GetPosition(this);
var x = GetLeft(_curAnn); var x = GetLeft(_curAnn);
var y = GetTop(_curAnn); var y = GetTop(_curAnn);
var offsetX = currentPos.X - _lastPos.X; var offsetX = point.X - _lastPos.X;
var offsetY = currentPos.Y - _lastPos.Y; var offsetY = point.Y - _lastPos.Y;
switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment) switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment)
{ {
case (HorizontalAlignment.Left, VerticalAlignment.Top): case (HorizontalAlignment.Left, VerticalAlignment.Top):
@@ -227,7 +399,7 @@ public class CanvasEditor : Canvas
_curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY); _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY);
break; break;
} }
_lastPos = currentPos; _lastPos = point;
} }
private void AnnotationPositionStart(object sender, MouseEventArgs e) private void AnnotationPositionStart(object sender, MouseEventArgs e)
@@ -244,19 +416,26 @@ public class CanvasEditor : Canvas
e.Handled = true; e.Handled = true;
} }
private void AnnotationPositionMove(object sender, MouseEventArgs e) private void AnnotationPositionMove(Point point)
{ {
if (SelectionState != SelectionState.AnnMoving) if (SelectionState != SelectionState.AnnMoving)
return; return;
var currentPos = e.GetPosition(this); var offsetX = point.X - _lastPos.X;
var offsetX = currentPos.X - _lastPos.X; var offsetY = point.Y - _lastPos.Y;
var offsetY = currentPos.Y - _lastPos.Y;
SetLeft(_curAnn, GetLeft(_curAnn) + offsetX); var nextLeft = GetLeft(_curAnn) + offsetX;
SetTop(_curAnn, GetTop(_curAnn) + offsetY); var nextTop = GetTop(_curAnn) + offsetY;
_lastPos = currentPos;
e.Handled = true; if (_clampedRect.HasValue)
{
nextLeft = Math.Clamp(nextLeft, _clampedRect.Value.X, _clampedRect.Value.Right - _curAnn.Width);
nextTop = Math.Clamp(nextTop, _clampedRect.Value.Y, _clampedRect.Value.Bottom - _curAnn.Height);
}
SetLeft(_curAnn, nextLeft);
SetTop(_curAnn, nextTop);
_lastPos = point;
} }
#endregion #endregion
@@ -266,64 +445,67 @@ public class CanvasEditor : Canvas
private void NewAnnotationStart(object sender, MouseButtonEventArgs e) private void NewAnnotationStart(object sender, MouseButtonEventArgs e)
{ {
_newAnnotationStartPos = e.GetPosition(this); _newAnnotationStartPos = e.GetPosition(this);
SetLeft(_newAnnotationRect, _newAnnotationStartPos.X); SetLeft(_newAnnotationRect, _newAnnotationStartPos.X);
SetTop(_newAnnotationRect, _newAnnotationStartPos.Y); SetTop(_newAnnotationRect, _newAnnotationStartPos.Y);
_newAnnotationRect.MouseMove += NewAnnotationCreatingMove; _newAnnotationRect.MouseMove += (sender, e) =>
{
var currentPos = e.GetPosition(this);
NewAnnotationCreatingMove(currentPos);
};
SelectionState = SelectionState.NewAnnCreating; SelectionState = SelectionState.NewAnnCreating;
} }
private void NewAnnotationCreatingMove(object sender, MouseEventArgs e) private void NewAnnotationCreatingMove(Point point)
{ {
if (SelectionState != SelectionState.NewAnnCreating) if (SelectionState != SelectionState.NewAnnCreating)
return; return;
var currentPos = e.GetPosition(this); var diff = point - _newAnnotationStartPos;
var diff = currentPos - _newAnnotationStartPos;
_newAnnotationRect.Height = Math.Abs(diff.Y); _newAnnotationRect.Height = Math.Abs(diff.Y);
_newAnnotationRect.Width = Math.Abs(diff.X); _newAnnotationRect.Width = Math.Abs(diff.X);
if (diff.X < 0) if (diff.X < 0)
SetLeft(_newAnnotationRect, currentPos.X); SetLeft(_newAnnotationRect, point.X);
if (diff.Y < 0) if (diff.Y < 0)
SetTop(_newAnnotationRect, currentPos.Y); SetTop(_newAnnotationRect, point.Y);
} }
private void CreateAnnotation(Point endPos) public void CreateDetections(Annotation annotation, List<DetectionClass> detectionClasses, Size mediaSize)
{ {
_newAnnotationRect.Width = 0; foreach (var detection in annotation.Detections)
_newAnnotationRect.Height = 0; {
var width = Math.Abs(endPos.X - _newAnnotationStartPos.X); var detectionClass = DetectionClass.FromYoloId(detection.ClassNumber, detectionClasses);
var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y); CanvasLabel canvasLabel;
if (width < MIN_SIZE || height < MIN_SIZE) if (!annotation.IsSplit || mediaSize.FitSizeForAI())
return; canvasLabel = new CanvasLabel(detection, RenderSize, mediaSize, detection.Confidence);
else
{
canvasLabel = new CanvasLabel(detection, annotation.SplitTile!.Size, null, detection.Confidence)
.ReframeFromSmall(annotation.SplitTile);
var time = GetTimeFunc(); //From CurrentMediaSize to Render Size
CreateAnnotation(CurrentAnnClass, time, new CanvasLabel var yoloLabel = new YoloLabel(canvasLabel, mediaSize);
{ canvasLabel = new CanvasLabel(yoloLabel, RenderSize, mediaSize, canvasLabel.Confidence);
Width = width,
Height = height,
X = Math.Min(endPos.X, _newAnnotationStartPos.X),
Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y)
});
} }
public DetectionControl CreateAnnotation(DetectionClass annClass, TimeSpan? time, CanvasLabel canvasLabel) var control = CreateDetectionControl(detectionClass, annotation.Time, canvasLabel);
control.UpdateLayout();
CheckLabelBoundaries(control);
}
}
private DetectionControl CreateDetectionControl(DetectionClass detectionClass, TimeSpan time, CanvasLabel canvasLabel)
{ {
var annotationControl = new DetectionControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) var detectionControl = new DetectionControl(detectionClass, time, AnnotationResizeStart, canvasLabel);
{ detectionControl.MouseDown += AnnotationPositionStart;
Width = canvasLabel.Width, SetLeft(detectionControl, canvasLabel.Left );
Height = canvasLabel.Height SetTop(detectionControl, canvasLabel.Top);
}; Children.Add(detectionControl);
annotationControl.MouseDown += AnnotationPositionStart; CurrentDetections.Add(detectionControl);
SetLeft(annotationControl, canvasLabel.X ); _newAnnotationRect.Fill = new SolidColorBrush(detectionClass.Color);
SetTop(annotationControl, canvasLabel.Y); return detectionControl;
Children.Add(annotationControl);
CurrentDetections.Add(annotationControl);
_newAnnotationRect.Fill = new SolidColorBrush(annClass.Color);
return annotationControl;
} }
#endregion #endregion
@@ -355,11 +537,16 @@ public class CanvasEditor : Canvas
public void ClearExpiredAnnotations(TimeSpan time) public void ClearExpiredAnnotations(TimeSpan time)
{ {
var expiredAnns = CurrentDetections.Where(x => var expiredAnns = CurrentDetections.Where(x =>
x.Time.HasValue && Math.Abs((time - x.Time).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds)
.ToList(); .ToList();
RemoveAnnotations(expiredAnns); RemoveAnnotations(expiredAnns);
} }
public void ResetBackground() => Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); public void ZoomTo(Point point)
{
SetZoom();
var matrix = _matrixTransform.Matrix;
matrix.ScaleAt(2, 2, point.X, point.Y);
SetZoom(matrix);
}
} }
+150 -15
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,136 @@
CellStyle="{DynamicResource DataGridCellStyle1}" CellStyle="{DynamicResource DataGridCellStyle1}"
IsReadOnly="True" IsReadOnly="True"
CanUserResizeRows="False" CanUserResizeRows="False"
CanUserResizeColumns="False"> CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public"
PreviewKeyDown="OnKeyBanActivity"
>
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn <DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
Width="50"
Header="Клавіша"
CanUserSort="False">
<DataGridTemplateColumn.HeaderStyle> <DataGridTemplateColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"/>
</Style> </Style>
</DataGridTemplateColumn.HeaderStyle> </DataGridTemplateColumn.HeaderStyle>
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate> <DataTemplate>
<Border Background="{Binding Path=ColorBrush}"> <Border Background="{Binding Path=ColorBrush}">
<TextBlock Text="{Binding Path=ClassNumber}"></TextBlock> <TextBlock Text="{Binding Path=ClassNumber}"/>
</Border> </Border>
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTextColumn <DataGridTextColumn Width="*" Header="Назва" Binding="{Binding Path=ShortName}" CanUserSort="False">
Width="*"
Header="Назва"
Binding="{Binding Path=Name}"
CanUserSort="False">
<DataGridTextColumn.HeaderStyle> <DataGridTextColumn.HeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#252525"></Setter> <Setter Property="Background" Value="#252525"/>
</Style> </Style>
</DataGridTextColumn.HeaderStyle> </DataGridTextColumn.HeaderStyle>
</DataGridTextColumn> </DataGridTextColumn>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<!-- StackPanel with mode switcher RadioButtons -->
<StackPanel x:Name="ModeSwitcherPanel"
Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="0,2,0,2">
<RadioButton x:Name="NormalModeRadioButton"
Tag="0"
GroupName="Mode"
Checked="ModeRadioButton_Checked"
IsChecked="True"
Style="{StaticResource ButtonRadioButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m256,105.5c-83.9,0-152.2,68.3-152.2,152.2 0,83.9 68.3,152.2 152.2,152.2 83.9,0 152.2-68.3
152.2-152.2 0-84-68.3-152.2-152.2-152.2zm0,263.5c-61.4,0-111.4-50-111.4-111.4 0-61.4 50-111.4 111.4-111.4 61.4,0 111.4,50 111.4,111.4
0,61.4-50,111.4-111.4,111.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m256,74.8c11.3,0 20.4-9.1 20.4-20.4v-23c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v23c2.84217e-14,11.3 9.1,20.4 20.4,20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m256,437.2c-11.3,0-20.4,9.1-20.4,20.4v22.9c0,11.3 9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-22.9c0-11.2-9.1-20.4-20.4-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m480.6,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h23c11.3,0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="m54.4,235.6h-23c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h22.9c11.3,0 20.4-9.1 20.4-20.4 0.1-11.3-9.1-20.4-20.3-20.4z" />
<GeometryDrawing Brush="LightGray" Geometry="M400.4,82.8L384.1,99c-8,8-8,20.9,0,28.9s20.9,8,28.9,0l16.2-16.2c8-8,8-20.9,0-28.9S408.3,74.8,400.4,82.8z" />
<GeometryDrawing Brush="LightGray" Geometry="m99,384.1l-16.2,16.2c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l16.2-16.2c8-8 8-20.9 0-28.9s-20.9-7.9-28.9,0z" />
<GeometryDrawing Brush="LightGray" Geometry="m413,384.1c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2z" />
<GeometryDrawing Brush="LightGray" Geometry="m99,127.9c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-16.2-16.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l16.2,16.2z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="RegularModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="EveningModeRadioButton"
Tag="20"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
Style="{StaticResource ButtonRadioButtonStyle}">
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m444.8,256l50.2-50.2c8-8 8-20.9 0-28.9-8-8-20.9-8-28.9,0l-58.7,58.7h-85c-1.3-4.2-3-8.3-5-12.1l60.1-60.1h83c11.3,
0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-71v-71c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4v83l-60.1,60.1c-3.8-2-7.9-3.7-12.1-5v-85l58.7-58.7c8-8 8-20.9
0-28.9-8-8-20.9-8-28.9,0l-50.3,50.1-50.2-50.2c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l58.7,58.7v85c-4.2,1.3-8.3,
3-12.1,5l-60.1-60.1v-83c0-11.3-9.1-20.4-20.4-20.4-11.3,0-20.4,9.1-20.4,20.4v71h-71c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,
20.4h83l60.1,60.1c-2,3.8-3.7,7.9-5,12.1h-85l-58.7-58.7c-8-8-20.9-8-28.9,0-8,8-8,20.9 0,28.9l50.1,50.3-50.2,50.2c-8,8-8,20.9 0,28.9
8,8 20.9,8 28.9,0l58.7-58.7h85c1.3,4.2 3,8.3 5,12.1l-60.1,60.1h-83c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h71v71c0,11.3
9.1,20.4 20.4,20.4 11.3,0 20.4-9.1 20.4-20.4v-83l60.1-60.1c3.8,2 7.9,3.7 12.1,5v85l-58.7,58.7c-8,8-8,20.9 0,28.9 8,8 20.9,8 28.9,0l50.2-50.2
50.2,50.2c8,8 20.9,8 28.9,0 8-8 8-20.9 0-28.9l-58.7-58.7v-85c4.2-1.3 8.3-3 12.1-5l60.1,60.1v83c0,11.3 9.1,20.4 20.4,20.4s20.4-9.1 20.4-20.4v-71h71c11.3,0
20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-83l-60.1-60.1c2-3.8 3.7-7.9 5-12.1h85l58.7,58.7c8,8 20.9,8 28.9,0 8-8 8-20.9
0-28.9l-50-50.2zm-217.3,0c0-15.7 12.8-28.5 28.5-28.5s28.5,12.8 28.5,28.5-12.8,28.5-28.5,28.5-28.5-12.8-28.5-28.5z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="WinterModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
<RadioButton x:Name="NightModeRadioButton"
Tag="40"
GroupName="Mode"
Checked="ModeRadioButton_Checked" Margin="3,0,0,0"
Style="{StaticResource ButtonRadioButtonStyle}"
>
<StackPanel Orientation="Horizontal">
<Image Height="16" Width="16">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V640 H640 V0 H0 Z">
<GeometryDrawing Brush="LightGray" Geometry="m500,113.1c-2.4-7.5-8.9-13-16.8-14.1l-55.2-7.9-24.6-48.9c-3.5-7-10.7-11.4-18.5-11.4-7.8,0-15,4.4-18.5,11.4l-24.6,
48.9-55.2,7.9c-7.8,1.1-14.3,6.6-16.8,14.1-2.4,7.5-0.3,15.8 5.4,21.3l39.7,37.9-9.4,53.4c-1.4,7.7 1.8,15.6 8.1,20.2 6.3,4.7 14.7,5.3 21.7,1.7l49.5-25.5
49.5,25.5c3,1.5 6.2,2.3 9.5,2.3 4.3,0 8.6-1.4 12.2-4 6.3-4.6 9.5-12.5 8.1-20.2l-9.4-53.4 39.7-37.9c5.9-5.5 8-13.8 5.6-21.3zm-81.6,36.9c-5,4.8-7.3,
11.7-6.1,18.5l4.1,23.3-22-11.3c-5.9-3-13-3-18.9,0l-22,11.3 4.1-23.3c1.2-6.8-1.1-13.7-6.1-18.5l-16.9-16.2 23.8-3.4c6.7-1 12.5-5.1 15.5-11.2l11-21.9
11,21.9c3,6 8.8,10.2 15.5,11.2l23.8,3.4-16.8,16.2z" />
<GeometryDrawing Brush="LightGray" Geometry="m442,361c-14.9,3.4-30.3,5.1-45.7,5.1-113.8,0-206.4-92.6-206.4-206.3 0-41.8 12.4-82 35.9-116.3
4.8-7 4.8-16.3 0-23.4-4.8-7.1-13.4-10.5-21.8-8.6-54,12.2-103,42.7-138,86-35.4,43.8-55,99.2-55,155.7 0,66.2 25.8,128.4 72.6,175.2 46.8,46.8
109.1,72.6 175.3,72.6 81.9,0 158.4-40.4 204.8-108.1 4.8-7 4.8-16.3 0-23.4-4.8-7-13.4-10.4-21.7-8.5zm-183.1,98.5c-113.8,0-206.4-92.6-206.4-206.3
0-78.2 45.3-149.1 112.8-183.8-11.2,28.6-17,59.1-17, 90.4 0,66.2 25.8,128.4 72.6,175.2 46.7,46.7 108.8,72.5 174.9,72.6-37.3,33.1-85.8,51.9-136.9,51.9z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
<TextBlock Name="NightModeButton"
Padding="3"
/>
</StackPanel>
</RadioButton>
</StackPanel>
</Grid>
</UserControl>
@@ -1,9 +1,97 @@
namespace Azaion.Common.Controls; using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
namespace Azaion.Common.Controls;
public class DetectionClassChangedEventArgs(DetectionClass detectionClass, int classNumber) : EventArgs
{
public DetectionClass DetectionClass { get; } = detectionClass;
public int ClassNumber { get; } = classNumber;
}
public partial class DetectionClasses public partial class DetectionClasses
{ {
public event EventHandler<DetectionClassChangedEventArgs>? DetectionClassChanged;
private const int CaptionedMinWidth = 230;
ObservableCollection<DetectionClass> _detectionClasses = new();
public DetectionClasses() public DetectionClasses()
{ {
InitializeComponent(); InitializeComponent();
SizeChanged += (sender, args) =>
{
if (args.NewSize.Width < CaptionedMinWidth)
{
RegularModeButton.Text = "";
WinterModeButton.Text = "";
NightModeButton.Text = "";
} }
else
{
RegularModeButton.Text = Constants.REGULAR_MODE_CAPTION;
WinterModeButton.Text= Constants.WINTER_MODE_CAPTION;
NightModeButton.Text= Constants.NIGHT_MODE_CAPTION;
}
};
}
public void Init(List<DetectionClass> detectionClasses)
{
foreach (var dClass in detectionClasses)
{
var cl = (DetectionClass)dClass.Clone();
cl.Color = cl.Color.ToConfidenceColor();
_detectionClasses.Add(cl);
}
DetectionDataGrid.ItemsSource = _detectionClasses;
DetectionDataGrid.SelectedIndex = 0;
}
public int CurrentClassNumber { get; private set; } = 0;
public DetectionClass? CurrentDetectionClass { get; set; }
private void DetectionDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) =>
RaiseDetectionClassChanged();
private void ModeRadioButton_Checked(object sender, RoutedEventArgs e) =>
RaiseDetectionClassChanged();
private void RaiseDetectionClassChanged()
{
var detClass = (DetectionClass)DetectionDataGrid.SelectedItem;
if (detClass == null)
return;
var modeAmplifier = 0;
foreach (var child in ModeSwitcherPanel.Children)
if (child is RadioButton { IsChecked: true } rb)
if (int.TryParse(rb.Tag?.ToString(), out int modeIndex))
{
detClass.PhotoMode = (PhotoMode)modeIndex;
modeAmplifier += modeIndex;
}
CurrentDetectionClass = detClass;
CurrentClassNumber = detClass.Id + modeAmplifier;
DetectionClassChanged?.Invoke(this, new DetectionClassChangedEventArgs(detClass, CurrentClassNumber));
}
public void SelectNum(int keyNumber)
{
DetectionDataGrid.SelectedIndex = keyNumber;
}
private void OnKeyBanActivity(object sender, KeyEventArgs e)
{
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
e.Handled = true;
}
} }
+86 -47
View File
@@ -4,19 +4,22 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Shapes; using System.Windows.Shapes;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Label = System.Windows.Controls.Label; using Azaion.Common.Extensions;
using Annotation = Azaion.Common.Database.Annotation;
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
public class DetectionControl : Border public class DetectionControl : Border
{ {
private readonly Action<object, MouseButtonEventArgs> _resizeStart; private readonly Action<object, MouseButtonEventArgs> _resizeStart;
private const double RESIZE_RECT_SIZE = 9; private const double RESIZE_RECT_SIZE = 10;
private readonly Grid _grid; private readonly Grid _grid;
private readonly TextBlock _classNameLabel; private readonly DetectionLabelPanel _detectionLabelPanel;
private readonly Label _probabilityLabel; public readonly Canvas DetectionLabelContainer;
public TimeSpan? Time { get; set; }
public TimeSpan Time { get; set; }
private readonly List<Rectangle> _resizedRectangles = new();
private DetectionClass _detectionClass = null!; private DetectionClass _detectionClass = null!;
public DetectionClass DetectionClass public DetectionClass DetectionClass
@@ -24,9 +27,13 @@ public class DetectionControl : Border
get => _detectionClass; get => _detectionClass;
set set
{ {
_grid.Background = value.ColorBrush; var brush = new SolidColorBrush(value.Color.ToConfidenceColor());
_probabilityLabel.Background = value.ColorBrush; BorderBrush = brush;
_classNameLabel.Text = value.Name; BorderThickness = new Thickness(1);
foreach (var rect in _resizedRectangles)
rect.Stroke = brush;
_detectionLabelPanel.DetectionClass = value;
_detectionClass = value; _detectionClass = value;
} }
} }
@@ -34,6 +41,7 @@ public class DetectionControl : Border
private readonly Rectangle _selectionFrame; private readonly Rectangle _selectionFrame;
private bool _isSelected; private bool _isSelected;
public bool IsSelected public bool IsSelected
{ {
get => _isSelected; get => _isSelected;
@@ -44,83 +52,114 @@ public class DetectionControl : Border
} }
} }
public DetectionControl(DetectionClass detectionClass, TimeSpan? time, Action<object, MouseButtonEventArgs> resizeStart, double? probability = null) public void UpdateAdornerScale(double scale)
{ {
if (Math.Abs(scale) < 0.0001)
return;
var inverseScale = 1.0 / scale;
BorderThickness = new Thickness(4 * inverseScale);
foreach (var rect in _resizedRectangles)
{
rect.Width = 2 * RESIZE_RECT_SIZE * inverseScale;
rect.Height = 2 * RESIZE_RECT_SIZE * inverseScale;;
rect.Margin = new Thickness(-RESIZE_RECT_SIZE * 0.7);
}
}
public (HorizontalAlignment Horizontal, VerticalAlignment Vertical) DetectionLabelPosition
{
get => (DetectionLabelContainer.HorizontalAlignment, DetectionLabelContainer.VerticalAlignment);
set
{
DetectionLabelContainer.HorizontalAlignment = value.Horizontal;
DetectionLabelContainer.VerticalAlignment = value.Vertical;
}
}
public DetectionControl(DetectionClass detectionClass, TimeSpan time, Action<object,
MouseButtonEventArgs> resizeStart, CanvasLabel canvasLabel)
{
Width = canvasLabel.Width;
Height = canvasLabel.Height;
Time = time; Time = time;
_resizeStart = resizeStart; _resizeStart = resizeStart;
_classNameLabel = new TextBlock
DetectionLabelContainer = new Canvas
{ {
Text = detectionClass.Name,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, 15, 0, 0),
FontSize = 14,
Cursor = Cursors.SizeAll
};
_probabilityLabel = new Label
{
Content = probability.HasValue ? $"{probability.Value:F0}%" : string.Empty,
HorizontalAlignment = HorizontalAlignment.Right, HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top, VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(0, -32, 0, 0), ClipToBounds = false,
FontSize = 16,
Visibility = Visibility.Visible
}; };
_detectionLabelPanel = new DetectionLabelPanel
{
Confidence = canvasLabel.Confidence,
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
};
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
_selectionFrame = new Rectangle _selectionFrame = new Rectangle
{ {
Margin = new Thickness(-3),
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch,
Stroke = new SolidColorBrush(Colors.Black), Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 2, StrokeThickness = 2,
Visibility = Visibility.Collapsed Visibility = Visibility.Collapsed
}; };
_resizedRectangles =
[
CreateResizeRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
CreateResizeRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
CreateResizeRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
CreateResizeRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
CreateResizeRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
CreateResizeRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
CreateResizeRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
CreateResizeRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
];
_grid = new Grid _grid = new Grid
{ {
Background = Brushes.Transparent,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch,
Children = Children = { _selectionFrame }
{
_selectionFrame,
_classNameLabel,
AddRect("rLT", HorizontalAlignment.Left, VerticalAlignment.Top, Cursors.SizeNWSE),
AddRect("rCT", HorizontalAlignment.Center, VerticalAlignment.Top, Cursors.SizeNS),
AddRect("rRT", HorizontalAlignment.Right, VerticalAlignment.Top, Cursors.SizeNESW),
AddRect("rLC", HorizontalAlignment.Left, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rRC", HorizontalAlignment.Right, VerticalAlignment.Center, Cursors.SizeWE),
AddRect("rLB", HorizontalAlignment.Left, VerticalAlignment.Bottom, Cursors.SizeNESW),
AddRect("rCB", HorizontalAlignment.Center, VerticalAlignment.Bottom, Cursors.SizeNS),
AddRect("rRB", HorizontalAlignment.Right, VerticalAlignment.Bottom, Cursors.SizeNWSE)
}
}; };
if (probability.HasValue) _grid.Children.Add(DetectionLabelContainer);
_grid.Children.Add(_probabilityLabel); foreach (var rect in _resizedRectangles)
_grid.Children.Add(rect);
Child = _grid; Child = _grid;
Cursor = Cursors.SizeAll; Cursor = Cursors.SizeAll;
DetectionClass = detectionClass; DetectionClass = detectionClass;
} }
//small corners //small corners
private Rectangle AddRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs) private Rectangle CreateResizeRect(string name, HorizontalAlignment ha, VerticalAlignment va, Cursor crs)
{ {
var rect = new Rectangle() // small rectangles at the corners and sides var rect = new Rectangle() // small rectangles at the corners and sides
{ {
ClipToBounds = false,
Margin = new Thickness(-1.1 * RESIZE_RECT_SIZE),
HorizontalAlignment = ha, HorizontalAlignment = ha,
VerticalAlignment = va, VerticalAlignment = va,
Width = RESIZE_RECT_SIZE, Width = RESIZE_RECT_SIZE,
Height = RESIZE_RECT_SIZE, Height = RESIZE_RECT_SIZE,
Stroke = new SolidColorBrush(Color.FromArgb(230, 40, 40, 40)), // small rectangles color Stroke = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), // small rectangles color
Fill = new SolidColorBrush(Color.FromArgb(1, 255, 255, 255)), StrokeThickness = 0.8,
Fill = new SolidColorBrush(Color.FromArgb(150, 80, 80, 80)),
Cursor = crs, Cursor = crs,
Name = name, Name = name,
}; };
rect.MouseDown += (sender, args) => _resizeStart(sender, args); rect.MouseDown += (sender, args) => _resizeStart(sender, args);
rect.MouseUp += (sender, args) => { (sender as UIElement)?.ReleaseMouseCapture(); };
return rect; return rect;
} }
public YoloLabel GetLabel(Size canvasSize, Size? videoSize = null) public CanvasLabel ToCanvasLabel() =>
{ new(DetectionClass.YoloId, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
var label = new CanvasLabel(DetectionClass.Id, Canvas.GetLeft(this), Canvas.GetTop(this), Width, Height);
return new YoloLabel(label, canvasSize, videoSize); public YoloLabel ToYoloLabel(Size canvasSize, Size? videoSize = null) =>
} new(ToCanvasLabel(), canvasSize, videoSize);
} }
@@ -0,0 +1,59 @@
<UserControl x:Class="Azaion.Common.Controls.DetectionLabelPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d">
<UserControl.Resources>
<!-- Friendly (Light Blue Square) -->
<DrawingImage x:Key="Friendly">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="LightBlue" Geometry="M25,50 l150,0 0,100 -150,0 z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- Hostile (Red Diamond) -->
<DrawingImage x:Key="Hostile">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="Red" Geometry="M 100,28 L172,100 100,172 28,100 100,28 Z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<!-- Unknown (Yellow Quatrefoil) -->
<DrawingImage x:Key="Unknown">
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V320 H320 V0 H0 Z">
<GeometryDrawing Brush="Yellow" Geometry="M63,63 C63,20 137,20 137,63 C180,63 180,137 137,137 C137,180
63,180 63,137 C20,137 20,63 63,63 Z">
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</UserControl.Resources>
<Grid x:Name="DetectionGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" x:Name="AffiliationImage">
</Image>
<Label Grid.Column="1" x:Name="DetectionClassName" FontSize="16"></Label>
</Grid>
</UserControl>
@@ -0,0 +1,70 @@
using System.Windows.Media;
using Azaion.Common.DTO;
using Azaion.Common.Extensions;
namespace Azaion.Common.Controls
{
public partial class DetectionLabelPanel
{
private AffiliationEnum _affiliation = AffiliationEnum.None;
public AffiliationEnum Affiliation
{
get => _affiliation;
set
{
_affiliation = value;
UpdateAffiliationImage();
}
}
private DetectionClass _detectionClass = new();
public DetectionClass DetectionClass {
get => _detectionClass;
set
{
_detectionClass = value;
SetClassName();
}
}
private double _confidence;
public double Confidence
{
get => _confidence;
set
{
_confidence = value;
SetClassName();
}
}
private void SetClassName()
{
DetectionClassName.Content = _confidence >= 0.995 ? _detectionClass.UIName : $"{_detectionClass.UIName}: {_confidence * 100:F0}%";
DetectionGrid.Background = new SolidColorBrush(_detectionClass.Color.ToConfidenceColor(_confidence));
}
public DetectionLabelPanel()
{
InitializeComponent();
}
private string _detectionLabelText(string detectionClassName) =>
_confidence >= 0.98 ? detectionClassName : $"{detectionClassName}: {_confidence * 100:F0}%";
private void UpdateAffiliationImage()
{
if (_affiliation == AffiliationEnum.None)
{
AffiliationImage.Source = null;
return;
}
if (TryFindResource(_affiliation.ToString()) is DrawingImage drawingImage)
AffiliationImage.Source = drawingImage;
else
AffiliationImage.Source = null;
}
}
}
+50
View File
@@ -0,0 +1,50 @@
<UserControl x:Class="Azaion.Common.Controls.NumericUpDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="DimGray">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="12" />
<RowDefinition Height="12" />
</Grid.RowDefinitions>
<TextBox Name="NudTextBox"
Background="DimGray"
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
TextAlignment="Right"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
Text="{Binding Value, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}"
LostFocus="NudTextBox_OnLostFocus"
PreviewTextInput="NudTextBox_OnPreviewTextInput"
DataObject.Pasting="NudTextBox_OnPasting"
/>
<RepeatButton
Name="NudButtonUp"
Grid.Column="1"
Grid.Row="0"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonUp_OnClick"
>^</RepeatButton>
<RepeatButton
Name="NudButtonDown"
Grid.Column="1"
Grid.Row="1"
FontSize="10"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
Click="NudButtonDown_OnClick"
>˅</RepeatButton>
</Grid>
</UserControl>
@@ -0,0 +1,126 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Azaion.Common.Controls;
public partial class NumericUpDown : UserControl
{
public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register(
nameof(MinValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(0m));
public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register(
nameof(MaxValue), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(100m));
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
nameof(Value), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(10m, OnValueChanged));
public static readonly DependencyProperty StepProperty = DependencyProperty.Register(
nameof(Step), typeof(decimal), typeof(NumericUpDown), new PropertyMetadata(1m));
public decimal MinValue
{
get => (decimal)GetValue(MinValueProperty);
set => SetValue(MinValueProperty, value);
}
public decimal MaxValue
{
get => (decimal)GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
public decimal Value
{
get => (decimal)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public decimal Step
{
get => (decimal)GetValue(StepProperty);
set => SetValue(StepProperty, value);
}
public NumericUpDown()
{
InitializeComponent();
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not NumericUpDown control)
return;
control.NudTextBox.Text = ((decimal)e.NewValue).ToString(CultureInfo.InvariantCulture);
control.NudTextBox.SelectionStart = control.NudTextBox.Text.Length;
}
private void NudButtonUp_OnClick(object sender, RoutedEventArgs e)
{
var step = Step <= 0 ? 1m : Step;
var newVal = Math.Min(MaxValue, Value + step);
Value = newVal;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudButtonDown_OnClick(object sender, RoutedEventArgs e)
{
var step = Step <= 0 ? 1m : Step;
var newVal = Math.Max(MinValue, Value - step);
Value = newVal;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudTextBox_OnLostFocus(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(NudTextBox.Text) || !decimal.TryParse(NudTextBox.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
{
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
return;
}
if (number > MaxValue )
{
Value = MaxValue;
NudTextBox.Text = MaxValue.ToString(CultureInfo.InvariantCulture);
}
else if (number < MinValue)
{
Value = MinValue;
NudTextBox.Text = Value.ToString(CultureInfo.InvariantCulture);
}
else
{
Value = number;
}
NudTextBox.SelectionStart = NudTextBox.Text.Length;
}
private void NudTextBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
var regex = new Regex("[^0-9.]+");
e.Handled = regex.IsMatch(e.Text);
}
private void NudTextBox_OnPasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
var text = (string)e.DataObject.GetData(typeof(string));
var regex = new Regex("[^0-9.]+");
if (regex.IsMatch(text))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using MediatR;
using MessagePack;
namespace Azaion.Common.DTO;
public enum AIAvailabilityEnum
{
None = 0,
Downloading = 10,
Converting = 20,
Uploading = 30,
Enabled = 200,
Warning = 300,
Error = 500
}
[MessagePackObject]
public class AIAvailabilityStatusEvent : INotification
{
[Key("s")] public AIAvailabilityEnum Status { get; set; }
[Key("m")] public string? ErrorMessage { get; set; }
public override string ToString() => $"{StatusMessageDict.GetValueOrDefault(Status, "Помилка")} {ErrorMessage}";
private static readonly Dictionary<AIAvailabilityEnum, string> StatusMessageDict = new()
{
{ AIAvailabilityEnum.Downloading, "Йде завантаження AI для Вашої відеокарти" },
{ AIAvailabilityEnum.Converting, "Йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
{ AIAvailabilityEnum.Uploading, "Йде зберігання AI" },
{ AIAvailabilityEnum.Enabled, "AI готовий для розпізнавання" },
{ AIAvailabilityEnum.Warning, "Неможливо запустити AI наразі, йде налаштування під Ваше залізо" },
{ AIAvailabilityEnum.Error, "Помилка під час налаштування AI" }
};
}
+9
View File
@@ -0,0 +1,9 @@
namespace Azaion.Common.DTO;
public enum AffiliationEnum
{
None = 0,
Friendly = 10,
Hostile = 20,
Unknown = 30
}
-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!;
}
+23 -28
View File
@@ -1,40 +1,35 @@
using System.Windows.Media; using System.Windows.Media;
using Newtonsoft.Json; using Azaion.Common.Database;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationResult // public class AnnotationResult
{ //{
[JsonProperty(PropertyName = "f")] //public Annotation Annotation { get; set; }
public string Image { get; set; } = null!;
[JsonProperty(PropertyName = "t")]
public TimeSpan Time { get; set; }
public double Lat { get; set; } //public string ImagePath { get; set; }
public double Lon { get; set; } //public string TimeStr { get; set; }
public List<Detection> Detections { get; set; } = new();
#region For XAML Form //public List<(Color Color, double Confidence)> Colors { get; private set; }
// public string ClassName { get; set; }
[JsonIgnore] // public AnnotationResult(Dictionary<int, DetectionClass> allDetectionClasses, Annotation annotation)
public string TimeStr => $"{Time:h\\:mm\\:ss}"; // {
[JsonIgnore] //Annotation = annotation;
public string ClassName { get; set; } = null!;
[JsonIgnore] //TimeStr = $"{annotation.Time:h\\:mm\\:ss}";
public Color ClassColor0 { get; set; } //ImagePath = annotation.ImagePath;
[JsonIgnore] // var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
public Color ClassColor1 { get; set; } // ClassName = detectionClasses.Count > 1
// ? string.Join(", ", detectionClasses.Select(x => allDetectionClasses[x].UIName))
// : allDetectionClasses[detectionClasses.FirstOrDefault()].UIName;
//
// Colors = annotation.Detections
// .Select(d => (allDetectionClasses[d.ClassNumber].Color, d.Confidence))
// .ToList();
[JsonIgnore] // }
public Color ClassColor2 { get; set; } // }
[JsonIgnore]
public Color ClassColor3 { get; set; }
#endregion
}
@@ -2,13 +2,15 @@
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Common.Database;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
{ {
public Annotation Annotation { get; set; } = annotation; public Annotation Annotation { get; set; } = annotation;
public bool IsValidator { get; set; } = isValidator;
private BitmapImage? _thumbnail; private BitmapImage? _thumbnail;
public BitmapImage? Thumbnail public BitmapImage? Thumbnail
@@ -19,20 +21,25 @@ public class AnnotationImageView(Annotation annotation) : INotifyPropertyChanged
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage()); Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
return _thumbnail; return _thumbnail;
} }
private set => _thumbnail = value; private set
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public void Delete()
{ {
File.Delete(Annotation.ImagePath); _thumbnail = value;
File.Delete(Annotation.LabelPath); OnPropertyChanged();
File.Delete(Annotation.ThumbPath);
} }
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
public string CreatedEmail => Annotation.CreatedEmail;
public bool IsSeed => IsValidator &&
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
!Annotation.CreatedRole.IsValidator();
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
public void UpdateUI() => OnPropertyChanged(nameof(IsSeed));
} }
+24
View File
@@ -0,0 +1,24 @@
using CommandLine;
using MessagePack;
namespace Azaion.Common.DTO;
[MessagePackObject]
[Verb("credsManual", HelpText = "Manual Credentials")]
public class ApiCredentials
{
[Key(nameof(Email))]
[Option('e', "email", Required = true, HelpText = "User Email")]
public string Email { get; set; } = null!;
[Key(nameof(Password))]
[Option('p', "pass", Required = true, HelpText = "User Password")]
public string Password { get; set; } = null!;
}
[Verb("credsEncrypted", isDefault: true, HelpText = "Encrypted Credentials")]
public class ApiCredentialsEncrypted
{
[Option('c', "creds", Group = "auto", HelpText = "Encrypted Creds")]
public string Creds { get; set; } = null!;
}
@@ -0,0 +1,7 @@
namespace Azaion.Common.DTO;
public class BusinessExceptionDto
{
public int ErrorCode { get; set; }
public string Message { get; set; } = string.Empty;
}
+11
View File
@@ -0,0 +1,11 @@
using System.Windows.Media;
namespace Azaion.Common.DTO;
public class ClusterDistribution
{
public string Label { get; set; } = "";
public Color Color { get; set; }
public int ClassCount { get; set; }
public double BarWidth { get; set; }
}
@@ -1,10 +1,25 @@
using MessagePack;
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO.Config;
[MessagePackObject]
public class AIRecognitionConfig public class AIRecognitionConfig
{ {
public double FrameRecognitionSeconds { get; set; } [Key("f_pr")] public int FramePeriodRecognition { get; set; }
public double TrackingDistanceConfidence { get; set; } [Key("f_rs")] public double FrameRecognitionSeconds { get; set; }
public double TrackingProbabilityIncrease { get; set; } [Key("pt")] public double ProbabilityThreshold { get; set; }
public double TrackingIntersectionThreshold { get; set; }
public int FramePeriodRecognition { get; set; } [Key("t_dc")] public double TrackingDistanceConfidence { get; set; }
[Key("t_pi")] public double TrackingProbabilityIncrease { get; set; }
[Key("t_it")] public double TrackingIntersectionThreshold { get; set; }
[Key("d")] public byte[] Data { get; set; } = null!;
[Key("p")] public List<string> Paths { get; set; } = null!;
[Key("m_bs")] public int ModelBatchSize { get; set; } = 4;
[Key("ov_p")] public double BigImageTileOverlapPercent { get; set; }
[Key("cam_a")] public double Altitude { get; set; }
[Key("cam_fl")] public double CameraFocalLength { get; set; }
[Key("cam_sw")] public double CameraSensorWidth { get; set; }
} }
+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; }
} }
+33 -51
View File
@@ -1,14 +1,17 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using Azaion.CommonSecurity; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO.Config;
public class AppConfig public class AppConfig
{ {
public ApiConfig ApiConfig { get; set; } = null!; public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public QueueConfig QueueConfig { get; set; } = null!; public QueueConfig QueueConfig { get; set; } = null!;
@@ -16,9 +19,17 @@ public class AppConfig
public AnnotationConfig AnnotationConfig { get; set; } = null!; public AnnotationConfig AnnotationConfig { get; set; } = null!;
public UIConfig UIConfig { get; set; } = null!;
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!; public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
public MapConfig MapConfig{ get; set; } = null!;
public GpsDeniedConfig GpsDeniedConfig { get; set; } = null!;
public CameraConfig CameraConfig { get; set; } = null!;
} }
public interface IConfigUpdater public interface IConfigUpdater
@@ -29,64 +40,35 @@ public interface IConfigUpdater
public class ConfigUpdater : IConfigUpdater public class ConfigUpdater : IConfigUpdater
{ {
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public void CheckConfig() public void CheckConfig()
{ {
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!; var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
var configFilePath = Path.Combine(exePath, SecurityConstants.CONFIG_PATH); var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
if (File.Exists(configFilePath)) if (File.Exists(configFilePath))
return; return;
var appConfig = new AppConfig Save(Constants.FailsafeAppConfig);
{
ApiConfig = new ApiConfig
{
Url = SecurityConstants.DEFAULT_API_URL,
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
},
AnnotationConfig = new AnnotationConfig
{
AnnotationClasses = Constants.DefaultAnnotationClasses,
VideoFormats = Constants.DefaultVideoFormats,
ImageFormats = Constants.DefaultImageFormats,
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
},
DirectoriesConfig = new DirectoriesConfig
{
VideosDirectory = Constants.DEFAULT_VIDEO_DIR,
ImagesDirectory = Constants.DEFAULT_IMAGES_DIR,
LabelsDirectory = Constants.DEFAULT_LABELS_DIR,
ResultsDirectory = Constants.DEFAULT_RESULTS_DIR,
ThumbnailsDirectory = Constants.DEFAULT_THUMBNAILS_DIR
},
ThumbnailConfig = new ThumbnailConfig
{
Size = Constants.DefaultThumbnailSize,
Border = Constants.DEFAULT_THUMBNAIL_BORDER
},
AIRecognitionConfig = new AIRecognitionConfig
{
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD,
FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION
}
};
Save(appConfig);
} }
public void Save(AppConfig config) public void Save(AppConfig config)
{ {
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8); ThrottleExt.Throttle(async () =>
{
var publicConfig = new
{
config.LoaderClientConfig,
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig,
config.CameraConfig
};
await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
} }
} }
+8
View File
@@ -0,0 +1,8 @@
namespace Azaion.Common.DTO.Config;
public class CameraConfig
{
public decimal Altitude { get; set; }
public decimal CameraFocalLength { get; set; }
public decimal CameraSensorWidth { get; set; }
}
@@ -0,0 +1,6 @@
namespace Azaion.Common.DTO.Config;
public class GpsDeniedConfig
{
public int MinKeyPoints { get; set; }
}
+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!;
}
+10
View File
@@ -0,0 +1,10 @@
namespace Azaion.Common.DTO.Config;
public class UIConfig
{
public double LeftPanelWidth { get; set; }
public double RightPanelWidth { get; set; }
public bool GenerateAnnotatedImage { get; set; }
public bool SilentDetection { get; set; }
public bool ShowDatasetWithDetectionsOnly { get; set; }
}
+32
View File
@@ -0,0 +1,32 @@
namespace Azaion.Common.DTO;
public class GeoPoint
{
const double PRECISION_TOLERANCE = 0.00005;
public double Lat { get; }
public double Lon { get; }
public GeoPoint() { }
public GeoPoint(double lat, double lon)
{
Lat = lat;
Lon = lon;
}
public override string ToString() => $"{Lat:F4}, {Lon:F4}";
public override bool Equals(object? obj)
{
if (obj is not GeoPoint point) return false;
return ReferenceEquals(this, obj) || Equals(point);
}
private bool Equals(GeoPoint point) =>
Math.Abs(Lat - point.Lat) < PRECISION_TOLERANCE && Math.Abs(Lon - point.Lon) < PRECISION_TOLERANCE;
public override int GetHashCode() => HashCode.Combine(Lat, Lon);
public static bool operator ==(GeoPoint left, GeoPoint right) => Equals(left, right);
public static bool operator !=(GeoPoint left, GeoPoint right) => !Equals(left, right);
}
+45 -4
View File
@@ -1,22 +1,63 @@
using System.Windows.Media; using System.Windows.Media;
using Azaion.Common.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class DetectionClass public class DetectionClass : ICloneable
{ {
public int Id { get; set; } public int Id { get; set; }
public string Name { get; set; } = null!; public string Name { get; set; } = null!;
public string ShortName { get; set; } = null!; public string ShortName { get; set; } = null!;
[JsonIgnore] public Color Color { get; set; }
public Color Color => Id.ToColor();
public int MaxSizeM { get; set; }
[JsonIgnore] [JsonIgnore]
public string UIName
{
get
{
var mode = PhotoMode switch
{
PhotoMode.Night => "(ніч)",
PhotoMode.Winter => "(зим)",
PhotoMode.Regular => "",
_ => ""
};
return ShortName + mode;
}
}
[JsonIgnore]
public PhotoMode PhotoMode { get; set; }
[JsonIgnore] //For UI
public int ClassNumber => Id + 1; public int ClassNumber => Id + 1;
[JsonIgnore]
public int YoloId => Id == -1 ? Id : (int)PhotoMode + Id;
[JsonIgnore] [JsonIgnore]
public SolidColorBrush ColorBrush => new(Color); public SolidColorBrush ColorBrush => new(Color);
public static DetectionClass FromYoloId(int yoloId, List<DetectionClass> detectionClasses)
{
var cls = yoloId % 20;
var photoMode = (PhotoMode)(yoloId - cls);
var detClass = detectionClasses[cls];
detClass.PhotoMode = photoMode;
return detClass;
}
public object Clone() => MemberwiseClone();
}
public enum PhotoMode
{
Regular = 0,
Winter = 20,
Night = 40
} }
+17
View File
@@ -0,0 +1,17 @@
namespace Azaion.Common.DTO;
public class Direction
{
public double Distance { get; set; }
public double Azimuth { get; set; }
public Direction() { }
public Direction(double distance, double azimuth)
{
Distance = distance;
Azimuth = azimuth;
}
public override string ToString() => $"{Distance:F2}, {Azimuth:F1} deg";
}
@@ -1,10 +1,15 @@
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO;
public class DirectoriesConfig public class DirectoriesConfig
{ {
public string? ApiResourcesDirectory { get; set; } = null!;
public string VideosDirectory { get; set; } = null!; public string VideosDirectory { get; set; } = null!;
public string LabelsDirectory { get; set; } = null!; public string LabelsDirectory { get; set; } = null!;
public string ImagesDirectory { get; set; } = null!; public string ImagesDirectory { get; set; } = null!;
public string ResultsDirectory { get; set; } = null!; public string ResultsDirectory { get; set; } = null!;
public string ThumbnailsDirectory { get; set; } = null!; public string ThumbnailsDirectory { get; set; } = null!;
public string GpsSatDirectory { get; set; } = null!;
public string GpsRouteDirectory { get; set; } = null!;
} }
+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; }
}
@@ -0,0 +1,22 @@
namespace Azaion.Common.DTO;
public abstract class ExternalClientConfig
{
public string ZeroMqHost { get; set; } = "";
public int ZeroMqPort { get; set; }
}
public class LoaderClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
}
public class InferenceClientConfig : ExternalClientConfig
{
public string ApiUrl { get; set; } = null!;
}
public class GpsDeniedClientConfig : ExternalClientConfig
{
public int ZeroMqReceiverPort { get; set; }
}
+4 -9
View File
@@ -1,24 +1,19 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Windows; using System.Windows;
using Azaion.Common.Database;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class FormState public class FormState
{ {
public MediaFileInfo? CurrentMedia { get; set; } public MediaFileInfo? CurrentMedia { get; set; }
public string VideoName => string.IsNullOrEmpty(CurrentMedia?.Name) public string MediaName => CurrentMedia?.FName ?? "";
? ""
: Path.GetFileNameWithoutExtension(CurrentMedia.Name).Replace(" ", "");
public string CurrentMrl { get; set; } = null!; public Size CurrentMediaSize { get; set; }
public Size CurrentVideoSize { get; set; }
public TimeSpan CurrentVideoLength { get; set; } public TimeSpan CurrentVideoLength { get; set; }
public TimeSpan? BackgroundTime { get; set; } public TimeSpan? BackgroundTime { get; set; }
public int CurrentVolume { get; set; } = 100; public int CurrentVolume { get; set; } = 100;
public ObservableCollection<AnnotationResult> AnnotationResults { get; set; } = []; public ObservableCollection<Annotation> AnnotationResults { get; set; } = [];
public WindowEnum ActiveWindow { get; set; } public WindowEnum ActiveWindow { get; set; }
public string GetTimeName(TimeSpan? ts) => $"{VideoName}_{ts:hmmssf}";
} }
@@ -1,4 +1,4 @@
namespace Azaion.Common.Extensions; namespace Azaion.Common.DTO;
public static class EnumerableExtensions public static class EnumerableExtensions
{ {
+12
View File
@@ -0,0 +1,12 @@
using Azaion.Common.DTO.Config;
namespace Azaion.Common.DTO;
public class InitConfig
{
public LoaderClientConfig LoaderClientConfig { get; set; } = null!;
public InferenceClientConfig InferenceClientConfig { get; set; } = null!;
public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public CameraConfig CameraConfig { get; set; } = null!;
}
+68 -53
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)
{ {
@@ -22,32 +22,56 @@ public abstract class Label
public class CanvasLabel : Label public class CanvasLabel : Label
{ {
public double X { get; set; } public double Left { get; set; }
public double Y { get; set; } public double Top { get; set; }
public double Width { get; set; } public double Width { get; set; }
public double Height { get; set; } public double Height { get; set; }
public double? Probability { get; } public double Confidence { get; set; }
public CanvasLabel() public double Bottom
{ {
get => Top + Height;
set => Height = value - Top;
} }
public CanvasLabel(int classNumber, double x, double y, double width, double height, double? probability = null) : base(classNumber) public double Right
{ {
X = x; get => Left + Width;
Y = y; set => Width = value - Left;
}
public double CenterX => Left + Width / 2.0;
public double CenterY => Top + Height / 2.0;
public Size Size => new(Width, Height);
public CanvasLabel() { }
public CanvasLabel(double left, double right, double top, double bottom)
{
Left = left;
Top = top;
Width = right - left;
Height = bottom - top;
Confidence = 1;
ClassNumber = -1;
}
public CanvasLabel(int classNumber, double left, double top, double width, double height, double confidence = 1) : base(classNumber)
{
Left = left;
Top = top;
Width = width; Width = width;
Height = height; Height = height;
Probability = probability; Confidence = confidence;
} }
public CanvasLabel(YoloLabel label, Size canvasSize, Size? videoSize = null, double? probability = null) public CanvasLabel(YoloLabel label, Size canvasSize, Size? mediaSize = null, double confidence = 1)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.HasValue var videoAr = mediaSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height ? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr; : canvasAr;
ClassNumber = label.ClassNumber; ClassNumber = label.ClassNumber;
@@ -60,8 +84,8 @@ public class CanvasLabel : Label
var realHeight = cw / videoAr; //real video height in pixels on canvas var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
X = left * cw; Left = left * cw;
Y = top * realHeight + blackStripHeight; Top = top * realHeight + blackStripHeight;
Width = label.Width * cw; Width = label.Width * cw;
Height = label.Height * realHeight; Height = label.Height * realHeight;
} }
@@ -70,24 +94,32 @@ public class CanvasLabel : Label
var realWidth = ch * videoAr; //real video width in pixels on canvas var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
X = left * realWidth + blackStripWidth; Left = left * realWidth + blackStripWidth;
Y = top * ch; Top = top * ch;
Width = label.Width * realWidth; Width = label.Width * realWidth;
Height = label.Height * ch; Height = label.Height * ch;
} }
Probability = probability; Confidence = confidence;
}
} }
public CanvasLabel ReframeToSmall(CanvasLabel smallTile) =>
new(ClassNumber, Left - smallTile.Left, Top - smallTile.Top, Width, Height, Confidence);
public CanvasLabel ReframeFromSmall(CanvasLabel smallTile) =>
new(ClassNumber, Left + smallTile.Left, Top + smallTile.Top, Width, Height, Confidence);
}
[MessagePackObject]
public class YoloLabel : Label public class YoloLabel : Label
{ {
[JsonProperty(PropertyName = "x")] public double CenterX { get; set; } [JsonProperty(PropertyName = "x")][Key("x")] public double CenterX { get; set; }
[JsonProperty(PropertyName = "y")] public double CenterY { get; set; } [JsonProperty(PropertyName = "y")][Key("y")] public double CenterY { get; set; }
[JsonProperty(PropertyName = "w")] public double Width { get; set; } [JsonProperty(PropertyName = "w")][Key("w")] public double Width { get; set; }
[JsonProperty(PropertyName = "h")] public double Height { get; set; } [JsonProperty(PropertyName = "h")][Key("h")] public double Height { get; set; }
public YoloLabel() public YoloLabel()
{ {
@@ -104,13 +136,13 @@ public class YoloLabel : Label
public RectangleF ToRectangle() => public RectangleF ToRectangle() =>
new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height); new((float)(CenterX - Width / 2.0), (float)(CenterY - Height / 2.0), (float)Width, (float)Height);
public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? videoSize = null) public YoloLabel(CanvasLabel canvasLabel, Size canvasSize, Size? mediaSize = null)
{ {
var cw = canvasSize.Width; var cw = canvasSize.Width;
var ch = canvasSize.Height; var ch = canvasSize.Height;
var canvasAr = cw / ch; var canvasAr = cw / ch;
var videoAr = videoSize.HasValue var videoAr = mediaSize.HasValue
? videoSize.Value.Width / videoSize.Value.Height ? mediaSize.Value.Width / mediaSize.Value.Height
: canvasAr; : canvasAr;
ClassNumber = canvasLabel.ClassNumber; ClassNumber = canvasLabel.ClassNumber;
@@ -118,20 +150,20 @@ public class YoloLabel : Label
double left, top; double left, top;
if (videoAr > canvasAr) //100% width if (videoAr > canvasAr) //100% width
{ {
left = canvasLabel.X / cw; left = canvasLabel.Left / cw;
Width = canvasLabel.Width / cw; Width = canvasLabel.Width / cw;
var realHeight = cw / videoAr; //real video height in pixels on canvas var realHeight = cw / videoAr; //real video height in pixels on canvas
var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom var blackStripHeight = (ch - realHeight) / 2.0; //height of black strips at the top and bottom
top = (canvasLabel.Y - blackStripHeight) / realHeight; top = (canvasLabel.Top - blackStripHeight) / realHeight;
Height = canvasLabel.Height / realHeight; Height = canvasLabel.Height / realHeight;
} }
else //100% height else //100% height
{ {
top = canvasLabel.Y / ch; top = canvasLabel.Top / ch;
Height = canvasLabel.Height / ch; Height = canvasLabel.Height / ch;
var realWidth = ch * videoAr; //real video width in pixels on canvas var realWidth = ch * videoAr; //real video width in pixels on canvas
var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom var blackStripWidth = (cw - realWidth) / 2.0; //height of black strips at the top and bottom
left = (canvasLabel.X - blackStripWidth) / realWidth; left = (canvasLabel.Left - blackStripWidth) / realWidth;
Width = canvasLabel.Width / realWidth; Width = canvasLabel.Width / realWidth;
} }
@@ -145,8 +177,11 @@ public class YoloLabel : Label
return null; return null;
var strings = s.Replace(',', '.').Split(' '); var strings = s.Replace(',', '.').Split(' ');
if (strings.Length != 5) if (strings.Length < 5)
throw new Exception("Wrong labels format!"); throw new Exception("Wrong labels format!");
if (strings.Length > 5)
strings = strings[..5];
var res = new YoloLabel var res = new YoloLabel
{ {
@@ -183,23 +218,3 @@ public class YoloLabel : Label
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
} }
public class Detection : YoloLabel
{
public string AnnotationName { get; set; }
public double? Probability { get; set; }
//For db
public Detection(){}
public Detection(string annotationName, YoloLabel label, double? probability = null)
{
AnnotationName = annotationName;
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Probability = probability;
}
}
@@ -1,4 +1,4 @@
namespace Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO;
public class LoginResponse public class LoginResponse
{ {
+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,24 +1,29 @@
using Azaion.CommonSecurity.DTO; using Azaion.Common.Database;
namespace Azaion.Common.DTO.Queue; namespace Azaion.Common.DTO.Queue;
using MessagePack; using MessagePack;
[MessagePackObject] [MessagePackObject]
public class AnnotationCreatedMessage public class AnnotationMessage
{ {
[Key(0)] public DateTime CreatedDate { get; set; } [Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!; [Key(1)] public string Name { get; set; } = null!;
[Key(2)] public string ImageExtension { get; set; } = null!; [Key(2)] public string OriginalMediaName { get; set; } = null!;
[Key(3)] public string Detections { get; set; } = null!; [Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public byte[] Image { get; set; } = null!; [Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public RoleEnum CreatedRole { get; set; } [Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public string CreatedEmail { get; set; } = null!; [Key(6)] public byte[]? Image { get; set; } = null!;
[Key(7)] public SourceEnum Source { get; set; } [Key(7)] public RoleEnum Role { get; set; }
[Key(8)] public AnnotationStatus Status { get; set; } [Key(8)] public string Email { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; }
[Key(10)] public AnnotationStatus Status { get; set; }
} }
[MessagePackObject] [MessagePackObject]
public class AnnotationValidatedMessage public class AnnotationBulkMessage
{ {
[Key(0)] public string Name { get; set; } = null!; [Key(0)] public string[] AnnotationNames { get; set; } = null!;
[Key(1)] public AnnotationStatus AnnotationStatus { get; set; }
[Key(2)] public string Email { get; set; } = null!;
[Key(3)] public DateTime CreatedDate { get; set; }
} }
+58
View File
@@ -0,0 +1,58 @@
using MessagePack;
namespace Azaion.Common.DTO;
[MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
{
[Key("CommandType")]
public CommandType CommandType { get; set; } = commandType;
[Key("Data")]
public byte[]? Data { get; set; } = data;
[Key("Message")]
public string? Message { get; set; } = message;
public static RemoteCommand Create(CommandType commandType) =>
new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data), message);
public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})";
}
[MessagePackObject]
public class LoadFileData(string filename, string? folder = null )
{
[Key(nameof(Folder))]
public string? Folder { get; set; } = folder;
[Key(nameof(Filename))]
public string Filename { get; set; } = filename;
}
public enum CommandType
{
None = 0,
Ok = 3,
Login = 10,
CheckResource = 12,
ListRequest = 15,
ListFiles = 18,
Load = 20,
LoadBigSmall = 22,
UploadBigSmall = 24,
DataBytes = 25,
Inference = 30,
InferenceData = 35,
InferenceStatus = 37,
InferenceDone = 38,
StopInference = 40,
AIAvailabilityCheck = 80,
AIAvailabilityResult = 85,
Error = 90,
Exit = 100,
}
@@ -1,4 +1,4 @@
namespace Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO;
public enum RoleEnum public enum RoleEnum
{ {
@@ -7,6 +7,11 @@ public enum RoleEnum
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue. Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
CompanionPC = 30, CompanionPC = 30,
Admin = 40, // Admin = 40, //
ResourceUploader = 50, //Uploading dll and ai models
ApiAdmin = 1000 //everything ApiAdmin = 1000 //everything
} }
public static class RoleEnumExtensions
{
public static bool IsValidator(this RoleEnum role) =>
role.In(RoleEnum.Validator, RoleEnum.Admin, RoleEnum.ApiAdmin);
}
+28
View File
@@ -0,0 +1,28 @@
using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class SatTile
{
public int X { get; }
public int Y { get; }
public GeoPoint LeftTop { get; }
public GeoPoint BottomRight { get; }
public string Url { get; set; }
public SatTile(int x, int y, int zoom, string url)
{
X = x;
Y = y;
Url = url;
LeftTop = GeoUtils.TileToWorldPos(x, y, zoom);
BottomRight = GeoUtils.TileToWorldPos(x + 1, y + 1, zoom);
}
public override string ToString()
{
return $"Tile[X={X}, Y={Y}, TL=({LeftTop.Lat:F6}, {LeftTop.Lon:F6}), BR=({BottomRight.Lat:F6}, {BottomRight.Lon:F6})]";
}
}
+3 -2
View File
@@ -1,9 +1,10 @@
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO;
public enum SelectionState public enum SelectionState
{ {
None = 0, None = 0,
NewAnnCreating = 1, NewAnnCreating = 1,
AnnResizing = 2, AnnResizing = 2,
AnnMoving = 3 AnnMoving = 3,
PanZoomMoving = 4,
} }
+21
View File
@@ -0,0 +1,21 @@
namespace Azaion.Common.DTO;
public class User
{
public string Id { get; set; } = "";
public string Email { get; set; } = "";
public RoleEnum Role { get; set; }
public UserConfig? UserConfig { get; set; } = null!;
}
public class UserConfig
{
public UserQueueOffsets? QueueOffsets { get; set; } = new();
}
public class UserQueueOffsets
{
public ulong AnnotationsOffset { get; set; }
public ulong AnnotationsConfirmOffset { get; set; }
public ulong AnnotationsCommandsOffset { get; set; }
}
+118
View File
@@ -0,0 +1,118 @@
using System.IO;
using System.Windows.Media;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Queue;
using MessagePack;
namespace Azaion.Common.Database;
[MessagePackObject]
public class Annotation
{
private static string _labelsDir = null!;
private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
DetectionClassesDict = detectionClassesDict;
}
[Key("n")] public string Name { get; set; } = null!;
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
[IgnoreMember]public TimeSpan Time { get; set; }
[IgnoreMember]public string ImageExtension { get; set; } = null!;
[IgnoreMember]public DateTime CreatedDate { get; set; }
[IgnoreMember]public string CreatedEmail { get; set; } = null!;
[IgnoreMember]public RoleEnum CreatedRole { get; set; }
[IgnoreMember]public SourceEnum Source { get; set; }
[IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; }
[IgnoreMember]public DateTime ValidateDate { get; set; }
[IgnoreMember]public string ValidateEmail { get; set; } = null!;
[Key("d")] public IEnumerable<Detection> Detections { get; set; } = null!;
[Key("t")] public long Milliseconds { get; set; }
[Key("lat")]public double Lat { get; set; }
[Key("lon")]public double Lon { get; set; }
#region Calculated
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
private CanvasLabel? _splitTile;
[IgnoreMember] public CanvasLabel? SplitTile
{
get
{
if (!IsSplit)
return null;
if (_splitTile != null)
return _splitTile;
var startCoordIndex = Name.IndexOf(Constants.SPLIT_SUFFIX, StringComparison.Ordinal) + Constants.SPLIT_SUFFIX.Length;
var coordsStr = Name.Substring(startCoordIndex, 14).Split('_');
_splitTile = new CanvasLabel
{
Left = double.Parse(coordsStr[1]),
Top = double.Parse(coordsStr[2]),
Width = double.Parse(coordsStr[0]),
Height = double.Parse(coordsStr[0])
};
return _splitTile;
}
}
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
private List<(Color Color, double Confidence)>? _colors;
[IgnoreMember] public List<(Color Color, double Confidence)> Colors => _colors ??= Detections
.Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence))
.ToList();
private string? _className;
[IgnoreMember] public string ClassName
{
get
{
if (string.IsNullOrEmpty(_className))
{
var detectionClasses = Detections.Select(x => x.ClassNumber).Distinct().ToList();
_className = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => DetectionClassesDict[x].UIName))
: DetectionClassesDict[detectionClasses.FirstOrDefault()].UIName;
}
return _className;
}
}
#endregion Calculated
}
[MessagePackObject]
public class AnnotationImage : Annotation
{
[Key("i")] public byte[] Image { get; set; } = null!;
}
public enum AnnotationStatus
{
None = 0,
Created = 10,
Edited = 20,
Validated = 30,
Deleted = 40
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Database;
public class AnnotationQueueRecord
{
public Guid Id { get; set; }
public DateTime DateTime { get; set; }
public AnnotationStatus Operation { get; set; }
public List<string> AnnotationNames { get; set; } = null!;
}
+3 -1
View File
@@ -1,4 +1,5 @@
using Azaion.Common.DTO; using Azaion.Common.DTO;
using CsvHelper;
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
@@ -7,6 +8,7 @@ namespace Azaion.Common.Database;
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
{ {
public ITable<Annotation> Annotations => this.GetTable<Annotation>(); public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>(); public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
public ITable<Detection> Detections => this.GetTable<Detection>(); public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<MediaFile> MediaFiles => this.GetTable<MediaFile>();
} }
@@ -0,0 +1,45 @@
using LinqToDB;
using LinqToDB.Mapping;
using Newtonsoft.Json;
namespace Azaion.Common.Database;
public static class AnnotationsDbSchemaHolder
{
public static readonly MappingSchema MappingSchema;
static AnnotationsDbSchemaHolder()
{
MappingSchema = new MappingSchema();
var builder = new FluentMappingBuilder(MappingSchema);
var annotationBuilder = builder.Entity<Annotation>();
annotationBuilder.HasTableName(Constants.ANNOTATIONS_TABLENAME)
.HasPrimaryKey(x => x.Name)
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName)
.Property(x => x.Time).HasDataType(DataType.Int64).HasConversion(ts => ts.Ticks, t => new TimeSpan(t));
annotationBuilder
.Ignore(x => x.Milliseconds)
.Ignore(x => x.Classes)
.Ignore(x => x.Classes)
.Ignore(x => x.ImagePath)
.Ignore(x => x.LabelPath)
.Ignore(x => x.ThumbPath);
builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Entity<MediaFile>()
.HasTableName(Constants.MEDIAFILE_TABLENAME);
builder.Build();
}
}
+78 -35
View File
@@ -3,23 +3,26 @@ using System.Diagnostics;
using System.IO; using System.IO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using LinqToDB.DataProvider.SQLite; using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
public interface IDbFactory public interface IDbFactory
{ {
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func); Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func); Task RunWrite(Func<AnnotationsDb, Task> func);
void SaveToDisk(); Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func);
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
} }
public class DbFactory : IDbFactory public class DbFactory : IDbFactory
{ {
private readonly ILogger<DbFactory> _logger;
private readonly AnnotationConfig _annConfig; private readonly AnnotationConfig _annConfig;
private string MemoryConnStr => "Data Source=:memory:"; private string MemoryConnStr => "Data Source=:memory:";
@@ -30,8 +33,12 @@ public class DbFactory : IDbFactory
private readonly SQLiteConnection _fileConnection; private readonly SQLiteConnection _fileConnection;
private readonly DataOptions _fileDataOptions; private readonly DataOptions _fileDataOptions;
private static readonly SemaphoreSlim WriteSemaphore = new(1, 1);
private static readonly Guid SaveTaskId = Guid.NewGuid();
public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger) public DbFactory(IOptions<AnnotationConfig> annConfig, ILogger<DbFactory> logger)
{ {
_logger = logger;
_annConfig = annConfig.Value; _annConfig = annConfig.Value;
_memoryConnection = new SQLiteConnection(MemoryConnStr); _memoryConnection = new SQLiteConnection(MemoryConnStr);
@@ -39,8 +46,8 @@ public class DbFactory : IDbFactory
_memoryDataOptions = new DataOptions() _memoryDataOptions = new DataOptions()
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_memoryConnection) .UseConnection(_memoryConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema)
_ = _memoryDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText)); .UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
_fileConnection = new SQLiteConnection(FileConnStr); _fileConnection = new SQLiteConnection(FileConnStr);
@@ -48,21 +55,33 @@ public class DbFactory : IDbFactory
.UseDataProvider(SQLiteTools.GetDataProvider()) .UseDataProvider(SQLiteTools.GetDataProvider())
.UseConnection(_fileConnection) .UseConnection(_fileConnection)
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
_ = _fileDataOptions.UseTracing(TraceLevel.Info, t => logger.LogInformation(t.SqlText));
if (!File.Exists(_annConfig.AnnotationsDbFile)) if (!File.Exists(_annConfig.AnnotationsDbFile))
CreateDb(); SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
RecreateTables();
_fileConnection.Open(); _fileConnection.Open();
using var db = new AnnotationsDb(_fileDataOptions);
var entityTypes = typeof(AnnotationsDb)
.GetProperties()
.Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ITable<>))
.Select(p => p.PropertyType.GetGenericArguments()[0])
.ToArray();
SchemaMigrator.EnsureSchemaUpdated(db, entityTypes);
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
} }
private void CreateDb() private void RecreateTables()
{ {
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
using var db = new AnnotationsDb(_fileDataOptions); using var db = new AnnotationsDb(_fileDataOptions);
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
db.CreateTable<Annotation>(); db.CreateTable<Annotation>();
db.CreateTable<AnnotationName>(); if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
db.CreateTable<Detection>(); db.CreateTable<Detection>();
if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
db.CreateTable<AnnotationQueueRecord>();
} }
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func) public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
@@ -71,38 +90,62 @@ public class DbFactory : IDbFactory
return await func(db); return await func(db);
} }
public async Task Run(Func<AnnotationsDb, Task> func) public async Task RunWrite(Func<AnnotationsDb, Task> func)
{
await WriteSemaphore.WaitAsync();
try
{ {
await using var db = new AnnotationsDb(_memoryDataOptions); await using var db = new AnnotationsDb(_memoryDataOptions);
await func(db); await func(db);
} ThrottleExt.Throttle(async () =>
public void SaveToDisk()
{ {
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
} }
} catch (Exception e)
public static class AnnotationsDbSchemaHolder
{ {
public static readonly MappingSchema MappingSchema; _logger.LogError(e, e.Message);
throw;
static AnnotationsDbSchemaHolder() }
finally
{ {
MappingSchema = new MappingSchema(); WriteSemaphore.Release();
var builder = new FluentMappingBuilder(MappingSchema); }
}
builder.Entity<Annotation>()
.HasTableName(Constants.ANNOTATIONS_TABLENAME) public async Task<T> RunWrite<T>(Func<AnnotationsDb, Task<T>> func)
.HasPrimaryKey(x => x.Name) {
.Association(a => a.Detections, (a, d) => a.Name == d.AnnotationName); await WriteSemaphore.WaitAsync();
try
builder.Entity<Detection>() {
.HasTableName(Constants.DETECTIONS_TABLENAME); await using var db = new AnnotationsDb(_memoryDataOptions);
var result = await func(db);
builder.Entity<AnnotationName>() ThrottleExt.Throttle(async () =>
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); {
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
builder.Build(); await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
return result;
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
throw;
}
finally
{
WriteSemaphore.Release();
}
}
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
{
await RunWrite(async db =>
{
var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken);
var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken);
_logger.LogInformation($"Deleted {detDeleted} detections, {annDeleted} annotations");
});
} }
} }
+30
View File
@@ -0,0 +1,30 @@
using Azaion.Common.DTO;
using MessagePack;
using Newtonsoft.Json;
namespace Azaion.Common.Database;
[MessagePackObject]
public class Detection : YoloLabel
{
[JsonProperty(PropertyName = "an")][Key("an")] public string AnnotationName { get; set; } = null!;
[JsonProperty(PropertyName = "p")][Key("p")] public double Confidence { get; set; }
[JsonProperty(PropertyName = "dn")] [Key("dn")] public string Description { get; set; } = null!;
[JsonProperty(PropertyName = "af")] [Key("af")] public AffiliationEnum Affiliation { get; set; }
//For db & serialization
public Detection(){}
public Detection(string annotationName, YoloLabel label, string description = "", double confidence = 1)
{
AnnotationName = annotationName;
Description = description;
ClassNumber = label.ClassNumber;
CenterX = label.CenterX;
CenterY = label.CenterY;
Height = label.Height;
Width = label.Width;
Confidence = confidence;
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Azaion.Common.Database;
public class MediaFile
{
public string Name { get; set; } = null!;
public string MediaUrl { get; set; } = null!;
public DateTime? LastProcessedDate { get; set; }
public MediaStatus Status { get; set; } = MediaStatus.New;
public int? RecognisedObjects { get; set; }
}
public enum MediaStatus
{
None,
New,
AIProcessing,
AIProcessed,
ManualConfirmed,
Error
}
+103
View File
@@ -0,0 +1,103 @@
using System.Data;
using LinqToDB.Data;
using LinqToDB.Mapping;
namespace Azaion.Common.Database;
public static class SchemaMigrator
{
public static void EnsureSchemaUpdated(DataConnection dbConnection, params Type[] entityTypes)
{
var connection = dbConnection.Connection;
var mappingSchema = dbConnection.MappingSchema;
if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
foreach (var type in entityTypes)
{
var entityDescriptor = mappingSchema.GetEntityDescriptor(type);
var tableName = entityDescriptor.Name.Name;
var existingColumns = GetTableColumns(connection, tableName);
if (existingColumns.Count == 0) // table does not exist
{
var columnDefinitions = entityDescriptor.Columns.Select(GetColumnDefinition);
dbConnection.Execute($"CREATE TABLE {tableName} ({string.Join(", ", columnDefinitions)})");
continue;
}
foreach (var column in entityDescriptor.Columns)
{
if (existingColumns.Contains(column.ColumnName, StringComparer.OrdinalIgnoreCase))
continue;
var columnDefinition = GetColumnDefinition(column);
dbConnection.Execute($"ALTER TABLE {tableName} ADD COLUMN {columnDefinition}");
}
}
}
private static HashSet<string> GetTableColumns(IDbConnection connection, string tableName)
{
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using var cmd = connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({tableName})";
using var reader = cmd.ExecuteReader();
while (reader.Read())
columns.Add(reader.GetString(1)); // "name" is in the second column
return columns;
}
private static string GetColumnDefinition(ColumnDescriptor column)
{
var type = column.MemberType;
var underlyingType = Nullable.GetUnderlyingType(type) ?? type;
var sqliteType = GetSqliteType(underlyingType);
var defaultClause = GetSqlDefaultValue(type, underlyingType);
return $"\"{column.ColumnName}\" {sqliteType} {defaultClause}";
}
private static string GetSqliteType(Type type) =>
type switch
{
_ when type == typeof(int)
|| type == typeof(long)
|| type == typeof(bool)
|| type.IsEnum
=> "INTEGER",
_ when type == typeof(double)
|| type == typeof(float)
|| type == typeof(decimal)
=> "REAL",
_ when type == typeof(byte[])
=> "BLOB",
_ => "TEXT"
};
private static string GetSqlDefaultValue(Type originalType, Type underlyingType)
{
var isNullable = originalType.IsClass || Nullable.GetUnderlyingType(originalType) != null;
if (isNullable)
return "NULL";
var defaultValue = Activator.CreateInstance(underlyingType);
if (underlyingType == typeof(bool))
return $"NOT NULL DEFAULT {(Convert.ToBoolean(defaultValue) ? 1 : 0)}";
if (underlyingType.IsEnum)
return $"NOT NULL DEFAULT {(int)(defaultValue ?? 0)}";
if (underlyingType.IsValueType && defaultValue is IFormattable f)
return $"NOT NULL DEFAULT {f.ToString(null, System.Globalization.CultureInfo.InvariantCulture)}";
return $"NOT NULL DEFAULT '{defaultValue}'";
}
}
@@ -1,6 +1,7 @@
using MediatR; using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class AnnotationCreatedEvent(Annotation annotation) : INotification public class AnnotationCreatedEvent(Annotation annotation) : INotification
{ {
@@ -0,0 +1,15 @@
using Azaion.Common.Database;
using MediatR;
namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
{
public List<string> AnnotationNames { get; set; } = annotationNames;
public bool FromQueue { get; set; } = fromQueue;
}
public class AnnotationAddedEvent(Annotation annotation) : INotification
{
public Annotation Annotation { get; set; } = annotation;
}
@@ -0,0 +1,13 @@
using MediatR;
namespace Azaion.Common.DTO;
public class AnnotatorControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
}
public class DatasetExplorerControlEvent(PlaybackControlEnum playbackControlEnum) : INotification
{
public PlaybackControlEnum PlaybackControl { get; set; } = playbackControlEnum;
}
@@ -1,7 +1,8 @@
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO;
using MediatR; using MediatR;
namespace Azaion.Common.DTO; namespace Azaion.Common.Events;
public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification public class KeyEvent(object sender, KeyEventArgs args, WindowEnum windowEnum) : INotification
{ {
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.Events;
public class LoadErrorEvent(string error) : INotification
{
public string Error { get; set; } = error;
}
@@ -0,0 +1,9 @@
using MediatR;
namespace Azaion.Common.Events;
public class SetStatusTextEvent(string text, bool isError = false) : INotification
{
public string Text { get; set; } = text;
public bool IsError { get; set; } = isError;
}
@@ -0,0 +1,3 @@
namespace Azaion.CommonSecurity.Exceptions;
public class BusinessException(string message) : Exception(message);
+21 -1
View File
@@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
namespace Azaion.Common.Extensions; namespace Azaion.Common.Extensions;
@@ -7,8 +8,14 @@ public static class BitmapExtensions
{ {
public static async Task<BitmapImage> OpenImage(this string imagePath) public static async Task<BitmapImage> OpenImage(this string imagePath)
{ {
var image = new BitmapImage();
await using var stream = File.OpenRead(imagePath); await using var stream = File.OpenRead(imagePath);
return OpenImage(stream);
}
public static BitmapImage OpenImage(this Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
var image = new BitmapImage();
image.BeginInit(); image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad; image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream; image.StreamSource = stream;
@@ -16,4 +23,17 @@ public static class BitmapExtensions
image.Freeze(); image.Freeze();
return image; return image;
} }
public static Color CreateTransparent(this Color color, byte transparency) =>
Color.FromArgb(transparency, color.R, color.G, color.B);
public static async Task SaveImage(this BitmapSource bitmap, string path, CancellationToken ct = default)
{
await using var stream = new FileStream(path, FileMode.Create);
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
encoder.Save(stream);
await stream.FlushAsync(ct);
}
} }
@@ -0,0 +1,30 @@
namespace Azaion.Common.Extensions;
public static class CancellationTokenExtensions
{
public static void WaitForCancel(this CancellationToken token, TimeSpan timeout)
{
try
{
Task.Delay(timeout, token).Wait(token);
}
catch (OperationCanceledException)
{
//Don't need to catch exception, need only return from the waiting
}
}
public static Task AsTask(this CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
return new TaskCompletionSource<bool>().Task;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
+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"
];
} }
@@ -0,0 +1,12 @@
namespace Azaion.Common.Extensions;
public static class EnumExtensions
{
public static T GetValueOrDefault<T>(this string value, T defaultValue) where T : struct
{
if (string.IsNullOrEmpty(value))
return defaultValue;
return Enum.TryParse(value, true, out T result) ? result : defaultValue;
}
}
+86
View File
@@ -0,0 +1,86 @@
using Azaion.Common.DTO;
namespace Azaion.Common.Extensions;
public static class GeoUtils
{
private const double EARTH_RADIUS = 6378137;
public static (int x, int y) WorldToTilePos(double lat, double lon, int zoom)
{
var latRad = lat * Math.PI / 180.0;
var n = Math.Pow(2.0, zoom);
var xTile = (int)Math.Floor((lon + 180.0) / 360.0 * n);
var yTile = (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * n);
return (xTile, yTile);
}
public static double ToRadians(double degrees) => degrees * Math.PI / 180.0;
public static double ToDegrees(double radians) => radians * 180.0 / Math.PI;
public static Direction DirectionTo(this GeoPoint p1, GeoPoint p2)
{
var lat1Rad = ToRadians(p1.Lat);
var lat2Rad = ToRadians(p2.Lat);
var dLon = ToRadians(p2.Lon - p1.Lon);
var dLat = ToRadians(p2.Lat - p1.Lat);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
var c = 2 * Math.Asin(Math.Sqrt(a));
var distance = EARTH_RADIUS * c;
var y = Math.Sin(dLon) * Math.Cos(lat2Rad);
var x = Math.Cos(lat1Rad) * Math.Sin(lat2Rad) -
Math.Sin(lat1Rad) * Math.Cos(lat2Rad) * Math.Cos(dLon);
var azimuthRadians = Math.Atan2(y, x);
var azimuth = (ToDegrees(azimuthRadians) + 360) % 360;
return new Direction
{
Distance = distance,
Azimuth = azimuth
};
}
public static GeoPoint GoDirection(this GeoPoint startPoint, Direction direction)
{
var angularDistance = direction.Distance / EARTH_RADIUS;
var azimuthRadians = ToRadians(direction.Azimuth);
var startLatRad = ToRadians(startPoint.Lat);
var startLonRad = ToRadians(startPoint.Lon);
var destLatRad = Math.Asin(Math.Sin(startLatRad) * Math.Cos(angularDistance) +
Math.Cos(startLatRad) * Math.Sin(angularDistance) * Math.Cos(azimuthRadians));
var destLonRad = startLonRad + Math.Atan2(Math.Sin(azimuthRadians) * Math.Sin(angularDistance) * Math.Cos(startLatRad),
Math.Cos(angularDistance) - Math.Sin(startLatRad) * Math.Sin(destLatRad));
return new GeoPoint(ToDegrees(destLatRad), ToDegrees(destLonRad));
}
public static GeoPoint TileToWorldPos(int x, int y, int zoom)
{
var n = Math.Pow(2.0, zoom);
var lonDeg = x / n * 360.0 - 180.0;
var latRad = Math.Atan(Math.Sinh(Math.PI * (1.0 - 2.0 * y / n)));
var latDeg = latRad * 180.0 / Math.PI;
return new GeoPoint(latDeg, lonDeg);
}
public static (double minLat, double maxLat, double minLon, double maxLon) GetBoundingBox(GeoPoint centerGeoPoint, double radiusM)
{
var latRad = centerGeoPoint.Lat * Math.PI / 180.0;
var latDiff = (radiusM / EARTH_RADIUS) * (180.0 / Math.PI);
var minLat = Math.Max(centerGeoPoint.Lat - latDiff, -90.0);
var maxLat = Math.Min(centerGeoPoint.Lat + latDiff, 90.0);
var lonDiff = (radiusM / (EARTH_RADIUS * Math.Cos(latRad))) * (180.0 / Math.PI);
var minLon = Math.Max(centerGeoPoint.Lon - lonDiff, -180.0);
var maxLon = Math.Min(centerGeoPoint.Lon + lonDiff, 180.0);
return (minLat, maxLat, minLon, maxLon);
}
}
@@ -0,0 +1,28 @@
using System.Drawing;
namespace Azaion.Common.Extensions;
public static class GraphicsExtensions
{
public static void DrawTextBox(this Graphics g, string text, PointF position, Brush background, Brush foreground)
{
using var textFont = new Font(FontFamily.GenericSerif, 14);
using var stringFormat = new StringFormat();
stringFormat.LineAlignment = StringAlignment.Near;
stringFormat.Alignment = StringAlignment.Center;
var padding = 1.0f;
var textSize = g.MeasureString(text, textFont);
var backgroundRect = new RectangleF(
position.X - textSize.Width / 2.0f - padding,
position.Y - padding,
textSize.Width + 2 * padding,
textSize.Height + 2 * padding
);
g.FillRectangle(background, backgroundRect);
g.DrawString(text, textFont, foreground, position, stringFormat);
}
}
+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,47 @@
using System.Linq.Expressions;
namespace Azaion.Common.Extensions;
public static class QueryableExtensions
{
/// <summary>
/// Adds Where true predicate only if result of condition is true.
/// If false predicate provided, uses it in case of false result
/// Useful for filters, when filters should be applied only when it was set (not NULL)
/// </summary>
public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> query, bool? condition,
Expression<Func<TSource, bool>> truePredicate,
Expression<Func<TSource, bool>>? falsePredicate = null)
{
if (!condition.HasValue)
return query;
if (condition.Value)
return query.Where(truePredicate);
return falsePredicate != null
? query.Where(falsePredicate)
: query;
}
/// <summary>
/// Adds Where true predicate only if result of condition is true.
/// If false predicate provided, uses it in case of false result
/// Useful for filters, when filters should be applied only when it was set (not NULL)
/// </summary>
public static IEnumerable<TSource> WhereIf<TSource>(this IEnumerable<TSource> query, bool? condition,
Func<TSource, bool> truePredicate,
Func<TSource, bool>? falsePredicate = null)
{
if (!condition.HasValue)
return query;
if (condition.Value)
return query.Where(truePredicate);
return falsePredicate != null
? query.Where(falsePredicate)
: query;
}
}
+14
View File
@@ -0,0 +1,14 @@
using Polly;
public static class ResilienceExt
{
public static void WithRetry(this Action operation, int retryCount = 3, int delayMs = 150) =>
Policy.Handle<Exception>()
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
.Execute(operation);
public static TResult WithRetry<TResult>(this Func<TResult> operation, int retryCount = 3, int delayMs = 150) =>
Policy.Handle<Exception>()
.WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs))
.Execute(operation);
}
@@ -0,0 +1,10 @@
using System.Windows;
namespace Azaion.Common.Extensions;
public static class SizeExtensions
{
public static bool FitSizeForAI(this Size size) =>
// Allow to be up to FullHD to save as 1280*1280
size.Width <= Constants.AI_TILE_SIZE_DEFAULT * 1.5 && size.Height <= Constants.AI_TILE_SIZE_DEFAULT * 1.5;
}
@@ -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}";
}
+63 -11
View File
@@ -1,19 +1,71 @@
namespace Azaion.Common.Extensions; using System.Collections.Concurrent;
namespace Azaion.Common.Extensions;
public static class ThrottleExt public static class ThrottleExt
{ {
private static bool _throttleOn; private class ThrottleState(Func<Task> action)
public static async Task Throttle(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
{ {
if (_throttleOn) public Func<Task> Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action));
return; public bool IsCoolingDown = false;
public bool CallScheduledDuringCooldown = false;
public Task CooldownTask = Task.CompletedTask;
public readonly object StateLock = new();
}
_throttleOn = true; private static readonly ConcurrentDictionary<Guid, ThrottleState> ThrottlerStates = new();
await func();
_ = Task.Run(async () => public static void Throttle(Func<Task> action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false)
{ {
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); ArgumentNullException.ThrowIfNull(action);
_throttleOn = false; if (actionId == Guid.Empty)
}, cancellationToken); throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId));
if (interval <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive.");
var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action));
state.Action = action;
lock (state.StateLock)
{
if (!state.IsCoolingDown)
{
state.IsCoolingDown = true;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state);
}
else
{
if (scheduleCallAfterCooldown)
state.CallScheduledDuringCooldown = true;
}
}
}
private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state)
{
try
{
await state.Action();
}
catch (Exception ex)
{
Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}");
}
finally
{
await Task.Delay(interval);
lock (state.StateLock)
{
if (state.CallScheduledDuringCooldown)
{
state.CallScheduledDuringCooldown = false;
state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state);
}
else
{
state.IsCoolingDown = false;
}
}
}
} }
} }
+59
View File
@@ -0,0 +1,59 @@
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
namespace Azaion.Common;
public class Security
{
private static string GenDefaultKey()
{
var date = DateTime.UtcNow;
return $"sAzaion_default_dfvkjhg_{date:yyyy}-{date:MM}_{date:dd}_{date:HH}_key";
}
public static string Encrypt<T>(T model, string? key = null) where T : class
{
var json = JsonConvert.SerializeObject(model);
var inputBytes = Encoding.UTF8.GetBytes(json);
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
var iv = RandomNumberGenerator.GetBytes(16);
using var aes = Aes.Create();
aes.Key = keyBytes;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.ISO10126;
using var encryptor = aes.CreateEncryptor();
var ciphertext = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length);
var result = new byte[iv.Length + ciphertext.Length];
iv.CopyTo(result, 0);
ciphertext.CopyTo(result, iv.Length);
return Convert.ToBase64String(result);
}
public static T Decrypt<T>(string encryptedData, string? key = null) where T : class
{
var ciphertextWithIv = Convert.FromBase64String(encryptedData);
var keyBytes = SHA256.HashData(Encoding.UTF8.GetBytes(key ?? GenDefaultKey()));
var iv = ciphertextWithIv[..16];
var ciphertext = ciphertextWithIv[16..];
using var aes = Aes.Create();
aes.Key = keyBytes;
aes.IV = iv;
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.ISO10126;
using var decryptor = aes.CreateDecryptor();
var plaintext = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
var json = Encoding.UTF8.GetString(plaintext);
return JsonConvert.DeserializeObject<T>(json)!;
}
}
+188 -61
View File
@@ -1,15 +1,18 @@
using System.Drawing.Imaging; using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Net; using System.Net;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO; using Azaion.Common.Events;
using Azaion.CommonSecurity.Services; using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using MediatR; using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
@@ -17,127 +20,251 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public class AnnotationService // SHOULD BE ONLY ONE INSTANCE OF AnnotationService. Do not add ANY NotificationHandler to it!
// Queue consumer should be created only once.
public class AnnotationService : IAnnotationService
{ {
private readonly AzaionApiClient _apiClient;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer; private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IAzaionApi _api;
private readonly ILogger<AnnotationService> _logger;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!; private Consumer _consumer = null!;
private readonly UIConfig _uiConfig;
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
public AnnotationService(AzaionApiClient apiClient,
public AnnotationService(
IDbFactory dbFactory, IDbFactory dbFactory,
FailsafeAnnotationsProducer producer, FailsafeAnnotationsProducer producer,
IOptions<QueueConfig> queueConfig, IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IGalleryService galleryService, IGalleryService galleryService,
IMediator mediator) IMediator mediator,
IAzaionApi api,
ILogger<AnnotationService> logger)
{ {
_apiClient = apiClient;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_producer = producer; _producer = producer;
_galleryService = galleryService; _galleryService = galleryService;
_mediator = mediator; _mediator = mediator;
_api = api;
_logger = logger;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await Init()).Wait(); Task.Run(async () => await InitQueueConsumer()).Wait();
} }
private async Task Init() private async Task InitQueueConsumer(CancellationToken token = default)
{ {
if (!_api.CurrentUser.Role.IsValidator())
return;
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
{ {
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)}, Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
UserName = _queueConfig.ConsumerUsername, UserName = _queueConfig.ConsumerUsername,
Password = _queueConfig.ConsumerPassword Password = _queueConfig.ConsumerPassword
}); });
var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets();
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
{ {
OffsetSpec = new OffsetTypeFirst(), Reference = _api.CurrentUser.Email,
MessageHandler = async (stream, _, _, message) => OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)), MessageHandler = async (_, _, context, message) =>
{
await _messageProcessingSemaphore.WaitAsync(token);
try
{
var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
return;
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
{
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
{
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
await SaveAnnotationInner(
msg.CreatedDate,
msg.OriginalMediaName,
msg.Name,
msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
msg.Source,
msg.Image == null ? null : new MemoryStream(msg.Image),
msg.Role,
msg.Email,
context.Offset,
token: token);
}
else
{
var msg = MessagePackSerializer.Deserialize<AnnotationBulkMessage>(message.Data.Contents);
if (annotationStatus == AnnotationStatus.Validated)
await ValidateAnnotations(msg.AnnotationNames.ToList(), true, token);
if (annotationStatus == AnnotationStatus.Deleted)
await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), token);
}
}
offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message
ThrottleExt.Throttle(() =>
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
finally
{
_messageProcessingSemaphore.Release();
}
}
}); });
} }
//AI / Manual //AI
public async Task SaveAnnotation(string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream = null, CancellationToken token = default) => public async Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default)
await SaveAnnotationInner(DateTime.UtcNow, fName, imageExtension, detections, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
//Queue (only from operators)
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
{ {
if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator) a.Time = TimeSpan.FromMilliseconds(a.Milliseconds);
return; return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Name, a.Time, a.Detections.ToList(),
SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct);
await SaveAnnotationInner(
message.CreatedDate,
message.Name,
message.ImageExtension,
JsonConvert.DeserializeObject<List<Detection>>(message.Detections) ?? [],
message.Source,
new MemoryStream(message.Image),
message.CreatedRole,
message.CreatedEmail,
cancellationToken);
} }
private async Task SaveAnnotationInner(DateTime createdDate, string fName, string imageExtension, List<Detection> detections, SourceEnum source, Stream? stream, //Manual
RoleEnum createdRole, public async Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default) =>
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, annotationName, time, detections, SourceEnum.Manual, stream,
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, string annotationName, TimeSpan time,
List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole,
string createdEmail, string createdEmail,
ulong? offset = null,
CancellationToken token = default) CancellationToken token = default)
{ {
//Flow for roles: var status = AnnotationStatus.Created;
// Operator: var annotation = await _dbFactory.RunWrite(async db =>
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
// Validator:
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
var classes = detections.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
AnnotationStatus status;
var annotation = await _dbFactory.Run(async db =>
{ {
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); var ann = await db.Annotations
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator .LoadWith(x => x.Detections)
? AnnotationStatus.Validated .FirstOrDefaultAsync(x => x.Name == annotationName, token: token);
: AnnotationStatus.Created;
if (ann != null) await db.Detections.DeleteAsync(x => x.AnnotationName == annotationName, token: token);
await db.Annotations
.Where(x => x.Name == fName) if (ann != null) //Annotation is already exists
.Set(x => x.Classes, classes) {
.Set(x => x.Source, source) status = AnnotationStatus.Edited;
var annotationUpdatable = db.Annotations
.Where(x => x.Name == annotationName)
.Set(x => x.Source, source);
if (userRole.IsValidator() && source == SourceEnum.Manual)
{
annotationUpdatable = annotationUpdatable
.Set(x => x.ValidateDate, createdDate)
.Set(x => x.ValidateEmail, createdEmail);
}
await annotationUpdatable
.Set(x => x.AnnotationStatus, status) .Set(x => x.AnnotationStatus, status)
.UpdateAsync(token: token); .UpdateAsync(token: token);
ann.Detections = detections;
}
else else
{ {
ann = new Annotation ann = new Annotation
{ {
CreatedDate = createdDate, CreatedDate = createdDate,
Name = fName, Name = annotationName,
ImageExtension = imageExtension, OriginalMediaName = originalMediaName,
Time = time,
ImageExtension = Constants.JPG_EXT,
CreatedEmail = createdEmail, CreatedEmail = createdEmail,
CreatedRole = createdRole, CreatedRole = userRole,
AnnotationStatus = status, AnnotationStatus = status,
Source = source, Source = source,
Detections = detections Detections = detections
}; };
await db.InsertAsync(ann, token: token); await db.InsertAsync(ann, token: token);
} }
await db.BulkCopyAsync(detections, cancellationToken: token);
return ann; return ann;
}); });
//Save image should be done in 1 thread only
await _imageAccessSemaphore.WaitAsync(token);
try
{
Image image = null!;
if (stream != null) if (stream != null)
{ {
var img = System.Drawing.Image.FromStream(stream); image = Image.FromStream(stream);
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue if (File.Exists(annotation.ImagePath))
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
} }
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await _galleryService.CreateThumbnail(annotation, token);
await _producer.SendToQueue(annotation, token); await _galleryService.CreateThumbnail(annotation, image, token);
if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, image, token);
}
catch (Exception e)
{
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
throw;
}
finally
{
_imageAccessSemaphore.Release();
}
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
if (!offset.HasValue) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue([annotation.Name], status, token);
return annotation;
}
public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
{
if (!_api.CurrentUser.Role.IsValidator())
return;
var annNames = annotationNames.ToHashSet();
await _dbFactory.RunWrite(async db =>
{
await db.Annotations
.Where(x => annNames.Contains(x.Name))
.Set(x => x.AnnotationStatus, AnnotationStatus.Validated)
.Set(x => x.ValidateDate, DateTime.UtcNow)
.Set(x => x.ValidateEmail, _api.CurrentUser.Email)
.UpdateAsync(token: token);
});
if (!fromQueue)
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
} }
} }
public interface IAnnotationService
{
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
}
+127
View File
@@ -0,0 +1,127 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Azaion.Common.DTO;
using Newtonsoft.Json;
using Serilog;
namespace Azaion.Common.Services;
public interface IAzaionApi
{
ApiCredentials Credentials { get; }
User CurrentUser { get; }
void UpdateOffsets(UserQueueOffsets offsets);
//Stream GetResource(string filename, string folder);
}
public class AzaionApi(ILogger logger, HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi
{
private string _jwtToken = null!;
const string APP_JSON = "application/json";
public ApiCredentials Credentials => credentials;
public User CurrentUser
{
get
{
var user = cache.GetFromCache(Constants.CURRENT_USER_CACHE_KEY,
() => Get<User>("users/current"));
if (user == null)
throw new Exception("Can't get current user");
return user;
}
}
public void UpdateOffsets(UserQueueOffsets offsets)
{
Put($"/users/queue-offsets/set", new
{
Email = CurrentUser.Email,
Offsets = offsets
});
}
private HttpResponseMessage Send(HttpRequestMessage request)
{
if (string.IsNullOrEmpty(_jwtToken))
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
var response = client.Send(request);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Authorize();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken);
response = client.Send(request);
}
if (response.IsSuccessStatusCode)
return response;
var stream = response.Content.ReadAsStream();
var content = new StreamReader(stream).ReadToEnd();
if (response.StatusCode == HttpStatusCode.Conflict)
{
var result = JsonConvert.DeserializeObject<BusinessExceptionDto>(content);
throw new Exception($"Failed: {response.StatusCode}! Error Code: {result?.ErrorCode}. Message: {result?.Message}");
}
throw new Exception($"Failed: {response.StatusCode}! Result: {content}");
}
private T? Get<T>(string url)
{
var response = Send(new HttpRequestMessage(HttpMethod.Get, url));
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
return JsonConvert.DeserializeObject<T>(json);
}
private void Put<T>(string url, T obj)
{
Send(new HttpRequestMessage(HttpMethod.Put, url)
{
Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON)
});
}
private void Authorize()
{
try
{
if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0)
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
var payload = new
{
email = credentials.Email,
password = credentials.Password
};
var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON);
var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content };
var response = client.Send(message);
if (!response.IsSuccessStatusCode)
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
var stream = response.Content.ReadAsStream();
var json = new StreamReader(stream).ReadToEnd();
var result = JsonConvert.DeserializeObject<LoginResponse>(json);
if (string.IsNullOrEmpty(result?.Token))
throw new Exception("JWT Token not found in response");
_jwtToken = result.Token;
}
catch (Exception e)
{
logger.Error(e, e.Message);
throw;
}
}
}
+27
View File
@@ -0,0 +1,27 @@
using LazyCache;
namespace Azaion.Common.Services;
public interface ICache
{
T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null);
void Invalidate(string key);
}
public class MemoryCache : ICache
{
private readonly IAppCache _cache = new CachingService();
public T GetFromCache<T>(string key, Func<T> fetchFunc, TimeSpan? expiration = null)
{
expiration ??= TimeSpan.FromHours(4);
return _cache.GetOrAdd(key, entry =>
{
var result = fetchFunc();
entry.AbsoluteExpirationRelativeToNow = expiration;
return result;
});
}
public void Invalidate(string key) => _cache.Remove(key);
}
+97 -59
View File
@@ -4,12 +4,14 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.AMQP;
using RabbitMQ.Stream.Client.Reliable; using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
@@ -18,17 +20,24 @@ public class FailsafeAnnotationsProducer
{ {
private readonly ILogger<FailsafeAnnotationsProducer> _logger; private readonly ILogger<FailsafeAnnotationsProducer> _logger;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IAzaionApi _azaionApi;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private readonly UIConfig _uiConfig;
private Producer _annotationProducer = null!; private Producer _annotationProducer = null!;
private Producer _annotationConfirmProducer = null!;
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig) public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger,
IDbFactory dbFactory,
IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IAzaionApi azaionApi)
{ {
_logger = logger; _logger = logger;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_azaionApi = azaionApi;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await ProcessQueue()); Task.Run(async () => await ProcessQueue());
} }
@@ -42,83 +51,112 @@ public class FailsafeAnnotationsProducer
}); });
} }
private async Task Init(CancellationToken cancellationToken = default) private async Task ProcessQueue(CancellationToken ct = default)
{ {
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); while (!ct.IsCancellationRequested)
}
private async Task ProcessQueue(CancellationToken cancellationToken = default)
{
await Init(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
var messages = await GetFromQueue(cancellationToken);
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
{ {
var sent = false; var sent = false;
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send while (!sent || !ct.IsCancellationRequested) //Waiting for send
{ {
try try
{ {
var createdMessages = messagesChunk var (records, annotationsDict) = await _dbFactory.Run(async db =>
.Where(x => x.Status == AnnotationStatus.Created) {
.Select(x => new Message(MessagePackSerializer.Serialize(x))) var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
var editedCreatedNames = records
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
.Select(x => x.AnnotationNames.FirstOrDefault())
.ToList(); .ToList();
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
var validatedMessages = messagesChunk var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.Status == AnnotationStatus.Validated) .Where(x => editedCreatedNames.Contains(x.Name))
.Select(x => new Message(MessagePackSerializer.Serialize(x))) .ToDictionaryAsync(a => a.Name, token: ct);
.ToList(); return (records, annotationsDict);
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip); });
await _dbFactory.Run(async db => var messages = new List<Message>();
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); foreach (var record in records)
{
var appProperties = new ApplicationProperties
{
{ nameof(AnnotationStatus), record.Operation.ToString() },
{ nameof(User.Email), _azaionApi.CurrentUser.Email }
};
if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted))
{
var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage
{
AnnotationNames = record.AnnotationNames.ToArray(),
AnnotationStatus = record.Operation,
Email = _azaionApi.CurrentUser.Email,
CreatedDate = record.DateTime
})) { ApplicationProperties = appProperties };
messages.Add(message);
}
else
{
var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault());
if (annotation == null)
continue;
var image = record.Operation == AnnotationStatus.Created
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
: null;
var annMessage = new AnnotationMessage
{
Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time,
Role = annotation.CreatedRole,
Email = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus,
ImageExtension = annotation.ImageExtension,
Image = image,
Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source,
};
var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties };
messages.Add(message);
}
}
if (messages.Any())
{
await _annotationProducer.Send(messages, CompressionType.Gzip);
var ids = records.Select(x => x.Id).ToList();
var removed = await _dbFactory.RunWrite(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct));
sent = true; sent = true;
} }
}
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(10), ct);
} }
} await Task.Delay(TimeSpan.FromSeconds(10), ct);
} }
} }
await Task.Delay(TimeSpan.FromSeconds(5), ct);
} }
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default) public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
{ {
return await _dbFactory.Run(async db => if (_uiConfig.SilentDetection)
return;
await _dbFactory.RunWrite(async db =>
await db.InsertAsync(new AnnotationQueueRecord
{ {
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a) Id = Guid.NewGuid(),
.ToListAsync(token: cancellationToken); DateTime = DateTime.UtcNow,
Operation = status,
var messages = new List<AnnotationCreatedMessage>(); AnnotationNames = annotationNames
foreach (var annotation in annotations) }, token: cancellationToken));
{
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
var annCreateMessage = new AnnotationCreatedMessage
{
Name = annotation.Name,
CreatedRole = annotation.CreatedRole,
CreatedEmail = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate,
Image = image,
Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source
};
messages.Add(annCreateMessage);
}
return messages;
});
}
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default)
{
await _dbFactory.Run(async db =>
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
} }
} }
+92 -28
View File
@@ -8,7 +8,7 @@ using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.CommonSecurity.DTO; using Azaion.Common.Extensions;
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -60,7 +60,7 @@ public class GalleryService(
{ {
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles()) foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete(); file.Delete();
await dbFactory.Run(async db => await dbFactory.RunWrite(async db =>
{ {
await db.Detections.DeleteAsync(x => true, token: cancellationToken); await db.Detections.DeleteAsync(x => true, token: cancellationToken);
await db.Annotations.DeleteAsync(x => true, token: cancellationToken); await db.Annotations.DeleteAsync(x => true, token: cancellationToken);
@@ -72,10 +72,9 @@ public class GalleryService(
await _updateLock.WaitAsync(); await _updateLock.WaitAsync();
var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db => var existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
await db.Annotations.ToDictionaryAsync(x => x.Name))); await db.Annotations.ToDictionaryAsync(x => x.Name)));
var missedAnnotations = new ConcurrentBag<Annotation>(); var missedAnnotations = new ConcurrentDictionary<string, Annotation>();
try try
{ {
var prefixLen = Constants.THUMBNAIL_PREFIX.Length; var prefixLen = Constants.THUMBNAIL_PREFIX.Length;
var thumbnails = ThumbnailsDirectory.GetFiles() var thumbnails = ThumbnailsDirectory.GetFiles()
@@ -89,7 +88,7 @@ public class GalleryService(
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
{ {
var fName = Path.GetFileNameWithoutExtension(file.Name); var fName = file.Name.ToFName();
try try
{ {
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt"); var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
@@ -97,6 +96,7 @@ public class GalleryService(
{ {
File.Delete(file.FullName); File.Delete(file.FullName);
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!"); logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
await dbFactory.DeleteAnnotations([fName], cancellationToken);
return; return;
} }
@@ -105,8 +105,36 @@ public class GalleryService(
return; return;
var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList(); var detections = (await YoloLabel.ReadFromFile(labelName, cancellationToken)).Select(x => new Detection(fName, x)).ToList();
//get names and time
var fileName = Path.GetFileNameWithoutExtension(file.Name);
var strings = fileName.Split("_");
var timeStr = strings.LastOrDefault();
string originalMediaName;
TimeSpan time;
//For some reason, TimeSpan.ParseExact doesn't work on every platform.
if (!string.IsNullOrEmpty(timeStr) &&
timeStr.Length == 6 &&
int.TryParse(timeStr[..1], out var hours) &&
int.TryParse(timeStr[1..3], out var minutes) &&
int.TryParse(timeStr[3..5], out var seconds) &&
int.TryParse(timeStr[5..], out var milliseconds))
{
time = new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
originalMediaName = fileName[..^7];
}
else
{
originalMediaName = fileName;
time = TimeSpan.FromSeconds(0);
}
var annotation = new Annotation var annotation = new Annotation
{ {
Time = time,
OriginalMediaName = originalMediaName,
Name = fName, Name = fName,
ImageExtension = Path.GetExtension(file.Name), ImageExtension = Path.GetExtension(file.Name),
Detections = detections, Detections = detections,
@@ -117,11 +145,18 @@ public class GalleryService(
AnnotationStatus = AnnotationStatus.Validated AnnotationStatus = AnnotationStatus.Validated
}; };
//Remove duplicates
if (!existingAnnotations.ContainsKey(fName)) if (!existingAnnotations.ContainsKey(fName))
missedAnnotations.Add(annotation); {
if (missedAnnotations.ContainsKey(fName))
logger.LogInformation($"{fName} is already exists! Duplicate!");
else
missedAnnotations.TryAdd(fName, annotation);
}
if (!thumbnails.Contains(fName)) if (!thumbnails.Contains(fName))
await CreateThumbnail(annotation, cancellationToken); await CreateThumbnail(annotation, cancellationToken: cancellationToken);
} }
catch (Exception e) catch (Exception e)
@@ -133,7 +168,7 @@ public class GalleryService(
{ {
ProgressFn = async num => ProgressFn = async num =>
{ {
Console.WriteLine($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}"); logger.LogInformation($"Processed {num} item by Thread {Environment.CurrentManagedThreadId}");
ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount); ProcessedThumbnailsPercentage = imagesCount == 0 ? 0 : Math.Min(100, num * 100 / (double)imagesCount);
ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage); ThumbnailsUpdate?.Invoke(ProcessedThumbnailsPercentage);
await Task.CompletedTask; await Task.CompletedTask;
@@ -142,35 +177,43 @@ public class GalleryService(
ProgressUpdateInterval = 200 ProgressUpdateInterval = 200
}); });
} }
catch (Exception e)
{
logger.LogError(e, $"Failed to refresh thumbnails! Error: {e.Message}");
}
finally finally
{ {
var copyOptions = new BulkCopyOptions var copyOptions = new BulkCopyOptions
{ {
MaxBatchSize = 50 MaxBatchSize = 50
}; };
await dbFactory.Run(async db =>
{ //Db could be updated during the long files scraping
var xx = missedAnnotations.GroupBy(x => x.Name) existingAnnotations = new ConcurrentDictionary<string, Annotation>(await dbFactory.Run(async db =>
.Where(gr => gr.Count() > 1) await db.Annotations.ToDictionaryAsync(x => x.Name)));
var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList();
var annotationsToInsert = missedAnnotations
.Where(a => !existingAnnotations.ContainsKey(a.Key))
.Select(x => x.Value)
.ToList(); .ToList();
foreach (var gr in xx)
Console.WriteLine(gr.Key); await dbFactory.RunWrite(async db =>
await db.BulkCopyAsync(copyOptions, missedAnnotations); {
await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections)); await db.BulkCopyAsync(copyOptions, annotationsToInsert);
await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections));
}); });
dbFactory.SaveToDisk();
_updateLock.Release(); _updateLock.Release();
} }
} }
public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default) public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default)
{ {
try try
{ {
var width = (int)_thumbnailConfig.Size.Width; var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height; var height = (int)_thumbnailConfig.Size.Height;
var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
var bitmap = new Bitmap(width, height); var bitmap = new Bitmap(width, height);
@@ -194,11 +237,11 @@ public class GalleryService(
.ToList(); .ToList();
if (annotation.Detections.Any()) if (annotation.Detections.Any())
{ {
var labelsMinX = labels.Min(x => x.X); var labelsMinX = labels.Min(x => x.Left);
var labelsMaxX = labels.Max(x => x.X + x.Width); var labelsMaxX = labels.Max(x => x.Left + x.Width);
var labelsMinY = labels.Min(x => x.Y); var labelsMinY = labels.Min(x => x.Top);
var labelsMaxY = labels.Max(x => x.Y + x.Height); var labelsMaxY = labels.Max(x => x.Top + x.Height);
var labelsHeight = labelsMaxY - labelsMinY + 2 * border; var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
var labelsWidth = labelsMaxX - labelsMinX + 2 * border; var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
@@ -227,8 +270,7 @@ public class GalleryService(
var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color; var color = _annotationConfig.DetectionClassesDict[label.ClassNumber].Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B)); var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale)); g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
g.FillRectangle(brush, rectangle);
} }
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg); bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
@@ -238,14 +280,36 @@ public class GalleryService(
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
} }
} }
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
{
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
using var g = Graphics.FromImage(originalImage);
foreach (var detection in annotation.Detections)
{
var detClass = _annotationConfig.DetectionClassesDict[detection.ClassNumber];
var color = detClass.Color;
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
var det = new CanvasLabel(detection, new Size(originalImage.Width, originalImage.Height));
g.DrawRectangle(new Pen(brush, width: 3), (float)det.Left, (float)det.Top, (float)det.Width, (float)det.Height);
var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%";
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
}
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
if (File.Exists(imagePath))
ResilienceExt.WithRetry(() => File.Delete(imagePath));
originalImage.Save(imagePath, ImageFormat.Jpeg);
}
} }
public interface IGalleryService public interface IGalleryService
{ {
event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
double ProcessedThumbnailsPercentage { get; set; } Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default);
Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default);
Task RefreshThumbnails(); Task RefreshThumbnails();
Task ClearThumbnails(CancellationToken cancellationToken = default); Task ClearThumbnails(CancellationToken cancellationToken = default);
Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default);
} }
@@ -0,0 +1,14 @@
using MediatR;
namespace Azaion.Common.Services;
public class GPSMatcherEventHandler(IGpsMatcherService gpsMatcherService) :
INotificationHandler<GPSMatcherResultEvent>,
INotificationHandler<GPSMatcherFinishedEvent>
{
public async Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken) =>
await gpsMatcherService.SetGpsResult(result, cancellationToken);
public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) =>
await gpsMatcherService.FinishGPS(notification, cancellationToken);
}
@@ -0,0 +1,45 @@
using Azaion.Common.DTO;
using MediatR;
namespace Azaion.Common.Services;
public enum MatchTypeEnum
{
None = -1,
MatchTypeSingle = 0,
MatchTypeStitched = 1,
MatchTypeOpticalFlow = 2,
MatchTypeInterpolated = 3,
MatchTypeFailure = 4
}
public class GPSMatcherResultEvent : INotification
{
public int Index { get; set; }
public string Image { get; set; } = null!;
public GeoPoint GeoPoint { get; set; } = null!;
public int KeyPoints { get; set; }
public MatchTypeEnum MatchType { get; set; }
}
public class GPSMatcherResultProcessedEvent : GPSMatcherResultEvent
{
public GeoPoint ProcessedGeoPoint { get; set; } = null!;
public GPSMatcherResultProcessedEvent() { }
public GPSMatcherResultProcessedEvent(GPSMatcherResultEvent gpsMatcherResultEvent, GeoPoint processedGeoPoint)
{
Index = gpsMatcherResultEvent.Index;
Image = gpsMatcherResultEvent.Image;
GeoPoint = gpsMatcherResultEvent.GeoPoint;
KeyPoints = gpsMatcherResultEvent.KeyPoints;
MatchType = gpsMatcherResultEvent.MatchType;
ProcessedGeoPoint = processedGeoPoint;
}
}
public class GPSMatcherJobAcceptedEvent : INotification {}
public class GPSMatcherFinishedEvent : INotification {}
@@ -0,0 +1,113 @@
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using MediatR;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public interface IGpsMatcherService
{
Task RunGpsMatching(string userRouteDir, GeoPoint geoPoint, CancellationToken detectToken = default);
void StopGpsMatching();
Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default);
Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken);
}
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient,
ISatelliteDownloader satelliteTileDownloader,
IOptions<DirectoriesConfig> dirConfig,
IOptions<GpsDeniedConfig> gpsDeniedConfig,
IMediator mediator) : IGpsMatcherService
{
private readonly DirectoriesConfig _dirConfig = dirConfig.Value;
private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 140;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
private const int MAX_AVG_POINTS = 2;
private string _routeDir = "";
private string _userRouteDir = "";
private List<string> _allRouteImages = new();
private Dictionary<string, int> _currentRouteImages = new();
private GeoPoint _lastGeoPoint = new();
private CancellationToken _detectToken;
private int _currentIndex;
private readonly Queue<Direction> _directions = new();
public async Task RunGpsMatching(string userRouteDir, GeoPoint initGeoPoint, CancellationToken detectToken = default)
{
_routeDir = Path.Combine(Constants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory);
_userRouteDir = userRouteDir;
_allRouteImages = Directory.GetFiles(userRouteDir)
.OrderBy(x => x).ToList();
_lastGeoPoint = initGeoPoint;
_detectToken = detectToken;
await StartMatchingRound(0);
}
private async Task StartMatchingRound(int startIndex)
{
//empty route dir
if (Directory.Exists(_routeDir))
Directory.Delete(_routeDir, true);
Directory.CreateDirectory(_routeDir);
_currentRouteImages = _allRouteImages
.Skip(startIndex)
.Take(POINTS_COUNT)
.Select((fullName, index) =>
{
var filename = Path.GetFileName(fullName);
File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename));
return new { Filename = Path.GetFileNameWithoutExtension(fullName), Index = startIndex + index };
})
.ToDictionary(x => x.Filename, x => x.Index);
await satelliteTileDownloader.GetTiles(_lastGeoPoint, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken);
await gpsMatcherClient.StartMatching(new StartMatchingEvent
{
ImagesCount = POINTS_COUNT,
GeoPoint = _lastGeoPoint,
SatelliteImagesDir = _dirConfig.GpsSatDirectory,
RouteDir = _dirConfig.GpsRouteDirectory
});
}
public void StopGpsMatching()
{
gpsMatcherClient.Stop();
}
public async Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default)
{
_currentIndex = _currentRouteImages[result.Image];
_currentRouteImages.Remove(result.Image);
if (result.KeyPoints >= gpsDeniedConfig.Value.MinKeyPoints)
{
var direction = _lastGeoPoint.DirectionTo(result.GeoPoint);
_directions.Enqueue(direction);
if (_directions.Count > MAX_AVG_POINTS)
_directions.Dequeue();
_lastGeoPoint = result.GeoPoint;
}
else
{
var direction = new Direction(_directions.Average(x => x.Distance), _directions.Average(x => x.Azimuth));
_lastGeoPoint = _lastGeoPoint.GoDirection(direction);
}
await mediator.Publish(new GPSMatcherResultProcessedEvent(result, _lastGeoPoint), detectToken);
}
public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken)
{
if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count)
await StartMatchingRound(_currentIndex);
}
}
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.IO;
using Azaion.Common.DTO;
using Azaion.Common.Events;
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IGpsMatcherClient : IDisposable
{
Task StartMatching(StartMatchingEvent startEvent);
void Stop();
}
public class StartMatchingEvent
{
public string RouteDir { get; set; } = null!;
public string SatelliteImagesDir { get; set; } = null!;
public int ImagesCount { get; set; }
public GeoPoint GeoPoint { get; set; } = null!;
public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24;
public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{GeoPoint.Lat},{GeoPoint.Lon},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
}
public class GpsMatcherClient : IGpsMatcherClient
{
private readonly IMediator _mediator;
private readonly ILogger<GpsMatcherClient> _logger;
private readonly string _requestAddress;
private readonly RequestSocket _requestSocket = new();
private readonly string _subscriberAddress;
private readonly SubscriberSocket _subscriberSocket = new();
private readonly NetMQPoller _poller = new();
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
{
_mediator = mediator;
_logger = logger;
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.ExternalGpsDeniedPath,
Arguments = $"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
WorkingDirectory = Constants.EXTERNAL_GPS_DENIED_FOLDER,
CreateNoWindow = true
};
process.Start();
}
catch (Exception e)
{
_logger.LogError(e, e.ToString());
}
_requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}";
_requestSocket.Connect(_requestAddress);
_subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqReceiverPort}";
_subscriberSocket.Connect(_subscriberAddress);
_subscriberSocket.Subscribe("");
_subscriberSocket.ReceiveReady += async (sender, e) => await ProcessClientCommand(sender, e);
_poller.Add(_subscriberSocket);
_poller.RunAsync();
}
private async Task ProcessClientCommand(object? sender, NetMQSocketEventArgs e)
{
while (e.Socket.TryReceiveFrameString(TimeSpan.FromMilliseconds(100), out var str))
{
try
{
if (string.IsNullOrEmpty(str))
continue;
switch (str)
{
case "FINISHED":
await _mediator.Publish(new GPSMatcherFinishedEvent());
break;
case "OK":
await _mediator.Publish(new GPSMatcherJobAcceptedEvent());
break;
default:
var parts = str.Split(',');
if (parts.Length != 6)
throw new Exception("Matching Result Failed");
var filename = Path.GetFileNameWithoutExtension(parts[1]);
await _mediator.Publish(new GPSMatcherResultEvent
{
Index = int.Parse(parts[0]),
Image = filename,
GeoPoint = new GeoPoint(double.Parse(parts[2]), double.Parse(parts[3])),
KeyPoints = int.Parse(parts[4]),
MatchType = Enum.TryParse<MatchTypeEnum>(parts[5], out var type) ? type : MatchTypeEnum.None
});
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
}
public async Task StartMatching(StartMatchingEvent e)
{
_requestSocket.SendFrame(e.ToString());
_requestSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(300), out var response);
if (response != "OK")
{
_logger.LogError(response);
await _mediator.Publish(new SetStatusTextEvent(response ?? "", true));
}
}
public void Stop() => _requestSocket.SendFrame("STOP");
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_requestSocket.SendFrame("EXIT");
_requestSocket.Disconnect(_requestAddress);
_requestSocket.Dispose();
_subscriberSocket.Disconnect(_subscriberAddress);
_subscriberSocket.Dispose();
}
}
@@ -0,0 +1,114 @@
using System.Diagnostics;
using System.Text;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using MediatR;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services.Inference;
public interface IInferenceClient : IDisposable
{
void Send(RemoteCommand create);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly ILogger<InferenceClient> _logger;
private readonly DealerSocket _dealer = new();
private readonly NetMQPoller _poller = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
private readonly LoaderClientConfig _loaderClientConfig;
private readonly IMediator _mediator;
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
IMediator mediator,
IOptions<LoaderClientConfig> loaderConfig)
{
_logger = logger;
_inferenceClientConfig = inferenceConfig.Value;
_loaderClientConfig = loaderConfig.Value;
_mediator = mediator;
Start();
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.EXTERNAL_INFERENCE_PATH,
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
CreateNoWindow = true
};
process.Start();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.ReceiveReady += async (_, e) => await ProcessClientCommand(e.Socket);
_poller.Add(_dealer);
_ = Task.Run(() => _poller.RunAsync());
}
private async Task ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default)
{
while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes))
{
if (bytes.Length == 0)
continue;
var remoteCommand = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
switch (remoteCommand.CommandType)
{
case CommandType.InferenceData:
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
break;
case CommandType.InferenceStatus:
var statusEvent = MessagePackSerializer.Deserialize<InferenceStatusEvent>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(statusEvent, ct);
break;
case CommandType.InferenceDone:
await _mediator.Publish(new InferenceDoneEvent(), ct);
break;
case CommandType.AIAvailabilityResult:
var aiAvailabilityStatus = MessagePackSerializer.Deserialize<AIAvailabilityStatusEvent>(remoteCommand.Data, cancellationToken: ct);
await _mediator.Publish(aiAvailabilityStatus, ct);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public void Stop() =>
Send(RemoteCommand.Create(CommandType.StopInference));
public void Send(RemoteCommand command) =>
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
public void Dispose()
{
_poller.Stop();
_poller.Dispose();
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.Close();
_dealer.Dispose();
}
}

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